Compare commits

..

No commits in common. "dev" and "Fix-ItemFixedPriceNameFilter" have entirely different histories.

562 changed files with 11675 additions and 20763 deletions

View File

@ -1 +0,0 @@
node_modules

1
.gitignore vendored
View File

@ -31,7 +31,6 @@ yarn-error.log*
# Cypress directories and files
/test/cypress/videos
/test/cypress/screenshots
/junit
# VitePress directories and files
/docs/.vitepress/cache

File diff suppressed because it is too large Load Diff

116
Jenkinsfile vendored
View File

@ -1,7 +1,6 @@
#!/usr/bin/env groovy
def PROTECTED_BRANCH
def IS_LATEST
def BRANCH_ENV = [
test: 'test',
@ -11,22 +10,19 @@ def BRANCH_ENV = [
node {
stage('Setup') {
env.FRONT_REPLICAS = 1
env.NODE_ENV = BRANCH_ENV[env.BRANCH_NAME] ?: 'dev'
PROTECTED_BRANCH = [
'dev',
'test',
'master',
'main',
'beta'
]
].contains(env.BRANCH_NAME)
IS_PROTECTED_BRANCH = PROTECTED_BRANCH.contains(env.BRANCH_NAME)
IS_LATEST = ['master', 'main'].contains(env.BRANCH_NAME)
// https://www.jenkins.io/doc/book/pipeline/jenkinsfile/#using-environment-variables
// https://www.jenkins.io/doc/book/pipeline/jenkinsfile/#using-environment-variables
echo "NODE_NAME: ${env.NODE_NAME}"
echo "WORKSPACE: ${env.WORKSPACE}"
echo "CHANGE_TARGET: ${env.CHANGE_TARGET}"
configFileProvider([
configFile(fileId: 'salix-front.properties',
@ -37,7 +33,7 @@ node {
props.each {key, value -> echo "${key}: ${value}" }
}
if (IS_PROTECTED_BRANCH) {
if (PROTECTED_BRANCH) {
configFileProvider([
configFile(fileId: "salix-front.branch.${env.BRANCH_NAME}",
variable: 'BRANCH_PROPS_FILE')
@ -62,19 +58,6 @@ pipeline {
PROJECT_NAME = 'lilium'
}
stages {
stage('Version') {
when {
expression { IS_PROTECTED_BRANCH }
}
steps {
script {
def packageJson = readJSON file: 'package.json'
def version = "${packageJson.version}-build${env.BUILD_ID}"
writeFile(file: 'VERSION.txt', text: version)
echo "VERSION: ${version}"
}
}
}
stage('Install') {
environment {
NODE_ENV = ""
@ -85,93 +68,48 @@ pipeline {
}
stage('Test') {
when {
expression { !IS_PROTECTED_BRANCH }
expression { !PROTECTED_BRANCH }
}
environment {
NODE_ENV = ''
CI = 'true'
TZ = 'Europe/Madrid'
NODE_ENV = ""
}
parallel {
stage('Unit') {
steps {
sh 'pnpm run test:front:ci'
}
post {
always {
junit(
testResults: 'junit/vitest.xml',
allowEmptyResults: true
)
}
}
}
stage('E2E') {
environment {
CREDS = credentials('docker-registry')
COMPOSE_PROJECT = "${PROJECT_NAME}-${env.BUILD_ID}"
COMPOSE_PARAMS = "-p ${env.COMPOSE_PROJECT} -f test/cypress/docker-compose.yml --project-directory ."
}
steps {
script {
sh 'rm -f junit/e2e-*.xml'
sh 'rm -rf test/cypress/screenshots'
env.COMPOSE_TAG = PROTECTED_BRANCH.contains(env.CHANGE_TARGET) ? env.CHANGE_TARGET : 'dev'
def image = docker.build('lilium-dev', '-f docs/Dockerfile.dev docs')
sh 'docker login --username $CREDS_USR --password $CREDS_PSW $REGISTRY'
sh "docker-compose ${env.COMPOSE_PARAMS} pull back"
sh "docker-compose ${env.COMPOSE_PARAMS} pull db"
sh "docker-compose ${env.COMPOSE_PARAMS} up -d"
image.inside("--network ${env.COMPOSE_PROJECT}_default -e CI -e TZ --init") {
sh 'sh test/cypress/cypressParallel.sh 2'
}
}
}
post {
always {
sh "docker-compose ${env.COMPOSE_PARAMS} down -v"
archiveArtifacts artifacts: 'test/cypress/screenshots/**/*', allowEmptyArchive: true
junit(
testResults: 'junit/e2e-*.xml',
allowEmptyResults: true
)
}
}
steps {
sh 'pnpm run test:unit:ci'
}
post {
always {
junit(
testResults: 'junitresults.xml',
allowEmptyResults: true
)
}
}
}
stage('Build') {
when {
expression { IS_PROTECTED_BRANCH }
expression { PROTECTED_BRANCH }
}
environment {
VERSION = readFile 'VERSION.txt'
CREDENTIALS = credentials('docker-registry')
}
steps {
sh 'quasar build'
script {
sh 'quasar build'
def baseImage = "salix-frontend:${env.VERSION}"
def image = docker.build(baseImage, ".")
docker.withRegistry("https://${env.REGISTRY}", 'docker-registry') {
image.push()
image.push(env.BRANCH_NAME)
if (IS_LATEST) image.push('latest')
}
def packageJson = readJSON file: 'package.json'
env.VERSION = "${packageJson.version}-build${env.BUILD_ID}"
}
dockerBuild()
}
}
stage('Deploy') {
when {
expression { IS_PROTECTED_BRANCH }
}
environment {
VERSION = readFile 'VERSION.txt'
expression { PROTECTED_BRANCH }
}
steps {
script {
def packageJson = readJSON file: 'package.json'
env.VERSION = "${packageJson.version}-build${env.BUILD_ID}"
}
withKubeConfig([
serverUrl: "$KUBERNETES_API",
credentialsId: 'kubernetes',

View File

@ -23,7 +23,7 @@ quasar dev
### Run unit tests
```bash
pnpm run test:front
pnpm run test:unit
```
### Run e2e tests
@ -32,26 +32,8 @@ pnpm run test:front
pnpm run test:e2e
```
### Run e2e parallel
```bash
pnpm run test:e2e:parallel
```
### View e2e parallel report
```bash
pnpm run test:e2e:summary
```
### Build the app for production
```bash
quasar build
```
### Serve the app for production
```bash
quasar build quasar serve dist/spa --host 0.0.0.0 --proxy=./proxy-serve.js
```

View File

@ -1,44 +1,12 @@
import { defineConfig } from 'cypress';
let urlHost, reporter, reporterOptions, timeouts;
if (process.env.CI) {
urlHost = 'front';
reporter = 'junit';
reporterOptions = {
mochaFile: 'junit/e2e-[hash].xml',
};
timeouts = {
defaultCommandTimeout: 30000,
requestTimeout: 30000,
responseTimeout: 60000,
pageLoadTimeout: 60000,
};
} else {
urlHost = 'localhost';
reporter = 'cypress-mochawesome-reporter';
reporterOptions = {
charts: true,
reportPageTitle: 'Cypress Inline Reporter',
reportFilename: '[status]_[datetime]-report',
embeddedScreenshots: true,
reportDir: 'test/cypress/reports',
inlineAssets: true,
};
timeouts = {
defaultCommandTimeout: 10000,
requestTimeout: 10000,
responseTimeout: 30000,
pageLoadTimeout: 60000,
};
}
// https://docs.cypress.io/app/tooling/reporters
// https://docs.cypress.io/app/references/configuration
// https://www.npmjs.com/package/cypress-mochawesome-reporter
export default defineConfig({
e2e: {
baseUrl: `http://${urlHost}:9000`,
experimentalStudio: false,
trashAssetsBeforeRuns: false,
defaultBrowser: 'chromium',
baseUrl: 'http://localhost:9000/',
experimentalStudio: true,
fixturesFolder: 'test/cypress/fixtures',
screenshotsFolder: 'test/cypress/screenshots',
supportFile: 'test/cypress/support/index.js',
@ -48,17 +16,27 @@ export default defineConfig({
specPattern: 'test/cypress/integration/**/*.spec.js',
experimentalRunAllSpecs: true,
watchForFileChanges: true,
reporter,
reporterOptions,
reporter: 'cypress-mochawesome-reporter',
reporterOptions: {
charts: true,
reportPageTitle: 'Cypress Inline Reporter',
reportFilename: '[status]_[datetime]-report',
embeddedScreenshots: true,
reportDir: 'test/cypress/reports',
inlineAssets: true,
},
component: {
componentFolder: 'src',
testFiles: '**/*.spec.js',
supportFile: 'test/cypress/support/unit.js',
},
setupNodeEvents: async (on, config) => {
const plugin = await import('cypress-mochawesome-reporter/plugin');
plugin.default(on);
return config;
},
viewportWidth: 1280,
viewportHeight: 720,
...timeouts,
includeShadowDom: true,
waitForAnimations: true,
},
});

7
docker-compose.yml Normal file
View File

@ -0,0 +1,7 @@
version: '3.7'
services:
main:
image: registry.verdnatura.es/salix-frontend:${VERSION:?}
build:
context: .
dockerfile: ./Dockerfile

View File

@ -1,47 +0,0 @@
FROM debian:12.9-slim
ARG DEBIAN_FRONTEND=noninteractive
RUN apt-get update \
&& apt-get install -y --no-install-recommends \
ca-certificates \
curl \
gnupg2 \
&& curl -fsSL https://deb.nodesource.com/setup_20.x | bash - \
&& apt-get install -y --no-install-recommends nodejs \
&& npm install -g corepack@0.31.0 \
&& corepack enable pnpm \
&& rm -rf /var/lib/apt/lists/*
RUN apt-get update \
&& apt-get -y --no-install-recommends install \
apt-utils \
chromium \
libasound2 \
libgbm-dev \
libgtk-3-0 \
libgtk2.0-0 \
libnotify-dev \
libnss3 \
libxss1 \
libxtst6 \
mesa-vulkan-drivers \
vulkan-tools \
xauth \
xvfb \
&& apt-get clean \
&& rm -rf /var/lib/apt/lists/*
RUN groupadd -r -g 1000 app \
&& useradd -r -u 1000 -g app -m -d /home/app app
USER app
ENV SHELL=bash
ENV PNPM_HOME="/home/app/.local/share/pnpm"
ENV PATH="$PNPM_HOME:$PATH"
RUN pnpm setup \
&& pnpm install --global cypress@14.1.0 \
&& cypress install
WORKDIR /app

View File

@ -1,6 +1,6 @@
{
"name": "salix-front",
"version": "25.16.0",
"version": "25.08.0",
"description": "Salix frontend",
"productName": "Salix",
"author": "Verdnatura",
@ -13,11 +13,9 @@
"format": "prettier --write \"**/*.{js,vue,scss,html,md,json}\" --ignore-path .gitignore",
"test:e2e": "cypress open",
"test:e2e:ci": "npm run resetDatabase && cd ../salix-front && cypress run",
"test:e2e:parallel": "bash ./test/cypress/run.sh",
"test:e2e:summary": "bash ./test/cypress/summary.sh",
"test": "echo \"See package.json => scripts for available tests.\" && exit 0",
"test:front": "vitest",
"test:front:ci": "vitest run",
"test:unit": "vitest",
"test:unit:ci": "vitest run",
"commitlint": "commitlint --edit",
"prepare": "npx husky install",
"addReferenceTag": "node .husky/addReferenceTag.js",
@ -49,21 +47,18 @@
"@quasar/quasar-app-extension-testing-unit-vitest": "^0.4.0",
"@vue/test-utils": "^2.4.4",
"autoprefixer": "^10.4.14",
"cypress": "^14.1.0",
"cypress": "^13.6.6",
"cypress-mochawesome-reporter": "^3.8.2",
"eslint": "^9.18.0",
"eslint-config-prettier": "^10.0.1",
"eslint-plugin-cypress": "^4.1.0",
"eslint-plugin-vue": "^9.32.0",
"husky": "^8.0.0",
"junit-merge": "^2.0.0",
"mocha": "^11.1.0",
"postcss": "^8.4.23",
"prettier": "^3.4.2",
"sass": "^1.83.4",
"vitepress": "^1.6.3",
"vitest": "^0.34.0",
"xunit-viewer": "^10.6.1"
"vitest": "^0.34.0"
},
"engines": {
"node": "^20 || ^18 || ^16",
@ -76,4 +71,4 @@
"vite": "^6.0.11",
"vitest": "^0.31.1"
}
}
}

File diff suppressed because it is too large Load Diff

View File

@ -1,6 +1,6 @@
export default [
{
path: '/api',
rule: { target: 'http://127.0.0.1:3000' },
rule: { target: 'http://0.0.0.0:3000' },
},
];

View File

@ -11,7 +11,6 @@
import { configure } from 'quasar/wrappers';
import VueI18nPlugin from '@intlify/unplugin-vue-i18n/vite';
import path from 'path';
const target = `http://${process.env.CI ? 'back' : 'localhost'}:3000`;
export default configure(function (/* ctx */) {
return {
@ -31,6 +30,7 @@ export default configure(function (/* ctx */) {
// --> boot files are part of "main.js"
// https://v2.quasar.dev/quasar-cli/boot-files
boot: ['i18n', 'axios', 'vnDate', 'validations', 'quasar', 'quasar.defaults'],
// https://v2.quasar.dev/quasar-cli-vite/quasar-config-js#css
css: ['app.scss'],
@ -109,17 +109,13 @@ export default configure(function (/* ctx */) {
},
proxy: {
'/api': {
target: target,
target: 'http://0.0.0.0:3000',
logLevel: 'debug',
changeOrigin: true,
secure: false,
},
},
open: false,
allowedHosts: [
'front', // Agrega este nombre de host
'localhost', // Opcional, para pruebas locales
],
},
// https://v2.quasar.dev/quasar-cli-vite/quasar-config-js#framework

View File

@ -9,19 +9,19 @@ export default {
if (!form) return;
try {
const inputsFormCard = form.querySelectorAll(
`input:not([disabled]):not([type="checkbox"])`,
`input:not([disabled]):not([type="checkbox"])`
);
if (inputsFormCard.length) {
focusFirstInput(inputsFormCard[0]);
}
const textareas = document.querySelectorAll(
'textarea:not([disabled]), [contenteditable]:not([disabled])',
'textarea:not([disabled]), [contenteditable]:not([disabled])'
);
if (textareas.length) {
focusFirstInput(textareas[textareas.length - 1]);
}
const inputs = document.querySelectorAll(
'form#formModel input:not([disabled]):not([type="checkbox"])',
'form#formModel input:not([disabled]):not([type="checkbox"])'
);
const input = inputs[0];
if (!input) return;
@ -30,5 +30,22 @@ export default {
} catch (error) {
console.error(error);
}
form.addEventListener('keyup', function (evt) {
if (evt.key === 'Enter' && !that.$attrs['prevent-submit']) {
const input = evt.target;
if (input.type == 'textarea' && evt.shiftKey) {
evt.preventDefault();
let { selectionStart, selectionEnd } = input;
input.value =
input.value.substring(0, selectionStart) +
'\n' +
input.value.substring(selectionEnd);
selectionStart = selectionEnd = selectionStart + 1;
return;
}
evt.preventDefault();
that.onSubmit();
}
});
},
};

View File

@ -51,5 +51,4 @@ export default boot(({ app }) => {
await useCau(response, message);
};
app.provide('app', app);
});

View File

@ -2,6 +2,7 @@
import { reactive, ref } from 'vue';
import { useI18n } from 'vue-i18n';
import FetchData from 'components/FetchData.vue';
import VnRow from 'components/ui/VnRow.vue';
import VnSelect from 'src/components/common/VnSelect.vue';
import VnSelectProvince from 'src/components/VnSelectProvince.vue';
@ -20,11 +21,14 @@ const postcodeFormData = reactive({
provinceFk: null,
townFk: null,
});
const townsFetchDataRef = ref(false);
const townFilter = ref({});
const countriesRef = ref(false);
const provincesOptions = ref([]);
const townsOptions = ref([]);
const town = ref({});
const countryFilter = ref({});
function onDataSaved(formData) {
const newPostcode = {
@ -47,6 +51,7 @@ async function setCountry(countryFk, data) {
data.townFk = null;
data.provinceFk = null;
data.countryFk = countryFk;
await fetchTowns();
}
// Province
@ -55,11 +60,22 @@ async function setProvince(id, data) {
const newProvince = provincesOptions.value.find((province) => province.id == id);
if (newProvince) data.countryFk = newProvince.countryFk;
postcodeFormData.provinceFk = id;
await fetchTowns();
}
async function onProvinceCreated(data) {
postcodeFormData.provinceFk = data.id;
}
function provinceByCountry(countryFk = postcodeFormData.countryFk) {
return provincesOptions.value
.filter((province) => province.countryFk === countryFk)
.map(({ id }) => id);
}
// Town
async function handleTowns(data) {
townsOptions.value = data;
}
function setTown(newTown, data) {
town.value = newTown;
data.provinceFk = newTown?.provinceFk ?? newTown;
@ -72,6 +88,18 @@ async function onCityCreated(newTown, formData) {
formData.townFk = newTown;
setTown(newTown, formData);
}
async function fetchTowns(countryFk = postcodeFormData.countryFk) {
if (!countryFk) return;
const provinces = postcodeFormData.provinceFk
? [postcodeFormData.provinceFk]
: provinceByCountry();
townFilter.value.where = {
provinceFk: {
inq: provinces,
},
};
await townsFetchDataRef.value?.fetch();
}
async function filterTowns(name) {
if (name !== '') {
@ -80,11 +108,22 @@ async function filterTowns(name) {
like: `%${name}%`,
},
};
await townsFetchDataRef.value?.fetch();
}
}
</script>
<template>
<FetchData
ref="townsFetchDataRef"
:sort-by="['name ASC']"
:limit="30"
:filter="townFilter"
@on-fetch="handleTowns"
auto-load
url="Towns/location"
/>
<FormModelPopup
url-create="postcodes"
model="postcode"
@ -110,13 +149,14 @@ async function filterTowns(name) {
@filter="filterTowns"
:tooltip="t('Create city')"
v-model="data.townFk"
url="Towns/location"
:options="townsOptions"
option-label="name"
option-value="id"
:rules="validate('postcode.city')"
:acls="[{ model: 'Town', props: '*', accessType: 'WRITE' }]"
:emit-value="false"
required
data-cy="locationTown"
sort-by="name ASC"
>
<template #option="{ itemProps, opt }">
<QItem v-bind="itemProps">
@ -157,12 +197,16 @@ async function filterTowns(name) {
/>
<VnSelect
ref="countriesRef"
:limit="30"
:filter="countryFilter"
:sort-by="['name ASC']"
auto-load
url="Countries"
required
:label="t('Country')"
hide-selected
option-label="name"
option-value="id"
v-model="data.countryFk"
:rules="validate('postcode.countryFk')"
@update:model-value="(value) => setCountry(value, data)"

View File

@ -62,9 +62,12 @@ const where = computed(() => {
auto-load
:where="where"
url="Autonomies/location"
sort-by="name ASC"
:sort-by="['name ASC']"
:limit="30"
:label="t('Autonomy')"
hide-selected
option-label="name"
option-value="id"
v-model="data.autonomyFk"
:rules="validate('province.autonomyFk')"
>

View File

@ -64,10 +64,6 @@ const $props = defineProps({
type: Function,
default: null,
},
beforeSaveFn: {
type: Function,
default: null,
},
goTo: {
type: String,
default: '',
@ -180,20 +176,14 @@ async function saveChanges(data) {
hasChanges.value = false;
return;
}
let changes = data || getChanges();
if ($props.beforeSaveFn) changes = await $props.beforeSaveFn(changes, getChanges);
const changes = data || getChanges();
try {
if (changes?.creates?.length === 0 && changes?.updates?.length === 0) {
return;
}
await axios.post($props.saveUrl || $props.url + '/crud', changes);
} finally {
isLoading.value = false;
}
originalData.value = JSON.parse(JSON.stringify(formData.value));
if (changes?.creates?.length) await vnPaginateRef.value.fetch();
if (changes.creates?.length) await vnPaginateRef.value.fetch();
hasChanges.value = false;
emit('saveChanges', data);
@ -239,12 +229,12 @@ async function remove(data) {
componentProps: {
title: t('globals.confirmDeletion'),
message: t('globals.confirmDeletionMessage'),
data: { deletes: ids },
newData,
ids,
promise: saveChanges,
},
})
.onOk(async () => {
await saveChanges({ deletes: ids });
newData = newData.filter((form) => !ids.some((id) => id == form[pk]));
fetch(newData);
});
@ -384,8 +374,6 @@ watch(formUrl, async () => {
@click="onSubmit"
:disable="!hasChanges"
:title="t('globals.save')"
v-shortcut="'s'"
shortcut="s"
data-cy="crudModelDefaultSaveBtn"
/>
<slot name="moreAfterActions" />

View File

@ -42,6 +42,7 @@ const itemFilter = {
const itemFilterParams = reactive({});
const closeButton = ref(null);
const isLoading = ref(false);
const producersOptions = ref([]);
const ItemTypesOptions = ref([]);
const InksOptions = ref([]);
const tableRows = ref([]);
@ -120,17 +121,23 @@ const selectItem = ({ id }) => {
</script>
<template>
<FetchData
url="Producers"
@on-fetch="(data) => (producersOptions = data)"
:filter="{ fields: ['id', 'name'], order: 'name ASC', limit: 30 }"
auto-load
/>
<FetchData
url="ItemTypes"
:filter="{ fields: ['id', 'name'], order: 'name ASC' }"
order="name ASC"
:filter="{ fields: ['id', 'name'], order: 'name ASC', limit: 30 }"
order="name"
@on-fetch="(data) => (ItemTypesOptions = data)"
auto-load
/>
<FetchData
url="Inks"
:filter="{ fields: ['id', 'name'], order: 'name ASC' }"
order="name ASC"
:filter="{ fields: ['id', 'name'], order: 'name ASC', limit: 30 }"
order="name"
@on-fetch="(data) => (InksOptions = data)"
auto-load
/>
@ -145,11 +152,11 @@ const selectItem = ({ id }) => {
<VnInput :label="t('entry.buys.size')" v-model="itemFilterParams.size" />
<VnSelect
:label="t('globals.producer')"
:options="producersOptions"
hide-selected
option-label="name"
option-value="id"
v-model="itemFilterParams.producerFk"
url="Producers"
:fields="['id', 'name']"
sort-by="name ASC"
/>
<VnSelect
:label="t('globals.type')"
@ -188,7 +195,7 @@ const selectItem = ({ id }) => {
>
<template #body-cell-id="{ row }">
<QTd auto-width @click.stop>
<QBtn flat class="link">{{ row.id }}</QBtn>
<QBtn flat color="blue">{{ row.id }}</QBtn>
<ItemDescriptorProxy :id="row.id" />
</QTd>
</template>

View File

@ -124,7 +124,7 @@ const selectTravel = ({ id }) => {
<FetchData
url="AgencyModes"
@on-fetch="(data) => (agenciesOptions = data)"
:filter="{ fields: ['id', 'name'], order: ['name ASC'] }"
:filter="{ fields: ['id', 'name'], order: 'name ASC', limit: 30 }"
auto-load
/>
<FetchData
@ -181,7 +181,6 @@ const selectTravel = ({ id }) => {
color="primary"
:disabled="isLoading"
:loading="isLoading"
data-cy="save-filter-travel-form"
/>
</div>
<QTable
@ -192,11 +191,10 @@ const selectTravel = ({ id }) => {
:no-data-label="t('Enter a new search')"
class="q-mt-lg"
@row-click="(_, row) => selectTravel(row)"
data-cy="table-filter-travel-form"
>
<template #body-cell-id="{ row }">
<QTd auto-width @click.stop data-cy="travelFk-travel-form">
<QBtn flat class="link">{{ row.id }}</QBtn>
<QTd auto-width @click.stop>
<QBtn flat color="blue">{{ row.id }}</QBtn>
<TravelDescriptorProxy :id="row.id" />
</QTd>
</template>

View File

@ -1,6 +1,6 @@
<script setup>
import axios from 'axios';
import { onMounted, onUnmounted, computed, ref, watch, nextTick, useAttrs } from 'vue';
import { onMounted, onUnmounted, computed, ref, watch, nextTick } from 'vue';
import { onBeforeRouteLeave, useRouter, useRoute } from 'vue-router';
import { useI18n } from 'vue-i18n';
import { useQuasar } from 'quasar';
@ -12,7 +12,6 @@ import SkeletonForm from 'components/ui/SkeletonForm.vue';
import VnConfirm from './ui/VnConfirm.vue';
import { tMobile } from 'src/composables/tMobile';
import { useArrayData } from 'src/composables/useArrayData';
import { getDifferences, getUpdatedValues } from 'src/filters';
const { push } = useRouter();
const quasar = useQuasar();
@ -23,7 +22,6 @@ const { validate } = useValidator();
const { notify } = useNotify();
const route = useRoute();
const myForm = ref(null);
const attrs = useAttrs();
const $props = defineProps({
url: {
type: String,
@ -96,10 +94,6 @@ const $props = defineProps({
type: [String, Boolean],
default: '800px',
},
onDataSaved: {
type: Function,
default: () => {},
},
});
const emit = defineEmits(['onFetch', 'onDataSaved']);
const modelValue = computed(
@ -112,14 +106,14 @@ const isLoading = ref(false);
const isResetting = ref(false);
const hasChanges = ref(!$props.observeFormChanges);
const originalData = computed(() => state.get(modelValue));
const formData = ref();
const formData = ref({});
const defaultButtons = computed(() => ({
save: {
dataCy: 'saveDefaultBtn',
color: 'primary',
icon: 'save',
label: 'globals.save',
click: async () => await save(),
click: () => myForm.value.submit(),
type: 'submit',
},
reset: {
@ -140,8 +134,7 @@ onMounted(async () => {
if (!$props.formInitialData) {
if ($props.autoLoad && $props.url) await fetch();
else if (arrayData.store.data)
updateAndEmit('onFetch', { val: arrayData.store.data });
else if (arrayData.store.data) updateAndEmit('onFetch', arrayData.store.data);
}
if ($props.observeFormChanges) {
watch(
@ -161,7 +154,7 @@ onMounted(async () => {
if (!$props.url)
watch(
() => arrayData.store.data,
(val) => updateAndEmit('onFetch', { val }),
(val) => updateAndEmit('onFetch', val),
);
watch(
@ -207,7 +200,7 @@ async function fetch() {
});
if (Array.isArray(data)) data = data[0] ?? {};
updateAndEmit('onFetch', { val: data });
updateAndEmit('onFetch', data);
} catch (e) {
state.set(modelValue, {});
throw e;
@ -234,11 +227,7 @@ async function save() {
if ($props.urlCreate) notify('globals.dataCreated', 'positive');
updateAndEmit('onDataSaved', {
val: formData.value,
res: response?.data,
old: originalData.value,
});
updateAndEmit('onDataSaved', formData.value, response?.data);
if ($props.reload) await arrayData.fetch({});
hasChanges.value = false;
} finally {
@ -253,7 +242,7 @@ async function saveAndGo() {
function reset() {
formData.value = JSON.parse(JSON.stringify(originalData.value));
updateAndEmit('onFetch', { val: originalData.value });
updateAndEmit('onFetch', originalData.value);
if ($props.observeFormChanges) {
hasChanges.value = false;
isResetting.value = true;
@ -275,11 +264,11 @@ function filter(value, update, filterOptions) {
);
}
function updateAndEmit(evt, { val, res, old } = { val: null, res: null, old: null }) {
function updateAndEmit(evt, val, res) {
state.set(modelValue, val);
if (!$props.url) arrayData.store.data = val;
emit(evt, state.get(modelValue), res, old);
emit(evt, state.get(modelValue), res);
}
function trimData(data) {
@ -289,27 +278,6 @@ function trimData(data) {
}
return data;
}
function onBeforeSave(formData, originalData) {
return getUpdatedValues(
Object.keys(getDifferences(formData, originalData)),
formData,
);
}
async function onKeyup(evt) {
if (evt.key === 'Enter' && !('prevent-submit' in attrs)) {
const input = evt.target;
if (input.type == 'textarea' && evt.shiftKey) {
let { selectionStart, selectionEnd } = input;
input.value =
input.value.substring(0, selectionStart) +
'\n' +
input.value.substring(selectionEnd);
selectionStart = selectionEnd = selectionStart + 1;
return;
}
await save();
}
}
defineExpose({
save,
@ -325,13 +293,12 @@ defineExpose({
<QForm
ref="myForm"
v-if="formData"
@submit.prevent
@keyup.prevent="onKeyup"
@submit="save"
@reset="reset"
class="q-pa-md"
:style="maxWidth ? 'max-width: ' + maxWidth : ''"
id="formModel"
:mapper="onBeforeSave"
:prevent-submit="$attrs['prevent-submit']"
>
<QCard>
<slot

View File

@ -1,13 +1,12 @@
<script setup>
import { ref, computed, useAttrs, nextTick } from 'vue';
import { ref, computed } from 'vue';
import { useI18n } from 'vue-i18n';
import { useState } from 'src/composables/useState';
import FormModel from 'components/FormModel.vue';
const emit = defineEmits(['onDataSaved', 'onDataCanceled']);
const props = defineProps({
defineProps({
title: {
type: String,
default: '',
@ -16,41 +15,23 @@ const props = defineProps({
type: String,
default: '',
},
showSaveAndContinueBtn: {
type: Boolean,
default: false,
},
});
const { t } = useI18n();
const attrs = useAttrs();
const state = useState();
const formModelRef = ref(null);
const closeButton = ref(null);
const isSaveAndContinue = ref(props.showSaveAndContinueBtn);
const isLoading = computed(() => formModelRef.value?.isLoading);
const reset = computed(() => formModelRef.value?.reset);
const onDataSaved = async (formData, requestResponse) => {
if (!isSaveAndContinue.value) closeButton.value?.click();
if (isSaveAndContinue.value) {
await nextTick();
state.set(attrs.model, attrs.formInitialData);
}
isSaveAndContinue.value = props.showSaveAndContinueBtn;
const onDataSaved = (formData, requestResponse) => {
if (closeButton.value) closeButton.value.click();
emit('onDataSaved', formData, requestResponse);
};
const onClick = async (saveAndContinue) => {
isSaveAndContinue.value = saveAndContinue;
await formModelRef.value.save();
};
const isLoading = computed(() => formModelRef.value?.isLoading);
defineExpose({
isLoading,
onDataSaved,
isSaveAndContinue,
reset,
});
</script>
@ -78,16 +59,15 @@ defineExpose({
flat
:disabled="isLoading"
:loading="isLoading"
data-cy="FormModelPopup_cancel"
v-close-popup
z-max
@click="emit('onDataCanceled')"
v-close-popup
data-cy="FormModelPopup_cancel"
z-max
/>
<QBtn
:flat="showSaveAndContinueBtn"
:label="t('globals.save')"
:title="t('globals.save')"
@click="onClick(false)"
type="submit"
color="primary"
class="q-ml-sm"
:disabled="isLoading"
@ -95,18 +75,6 @@ defineExpose({
data-cy="FormModelPopup_save"
z-max
/>
<QBtn
v-if="showSaveAndContinueBtn"
:label="t('globals.isSaveAndContinue')"
:title="t('globals.isSaveAndContinue')"
color="primary"
class="q-ml-sm"
:disabled="isLoading"
:loading="isLoading"
data-cy="FormModelPopup_isSaveAndContinue"
z-max
@click="onClick(true)"
/>
</div>
</template>
</FormModel>

View File

@ -121,25 +121,23 @@ const removeTag = (index, params, search) => {
applyTags(params, search);
};
const setCategoryList = (data) => {
categoryList.value = (data || []).map((category) => ({
...category,
icon: `vn:${(category.icon || '').split('-')[1]}`,
}));
categoryList.value = (data || [])
.filter((category) => category.display)
.map((category) => ({
...category,
icon: `vn:${(category.icon || '').split('-')[1]}`,
}));
fetchItemTypes();
};
</script>
<template>
<FetchData
url="ItemCategories"
auto-load
@on-fetch="setCategoryList"
:where="{ display: { neq: 0 } }"
/>
<FetchData url="ItemCategories" limit="30" auto-load @on-fetch="setCategoryList" />
<FetchData
url="Tags"
:filter="{ fields: ['id', 'name', 'isFree'] }"
auto-load
limit="30"
@on-fetch="(data) => (tagOptions = data)"
/>
<VnFilterPanel
@ -197,8 +195,11 @@ const setCategoryList = (data) => {
:label="t('components.itemsFilterPanel.typeFk')"
v-model="params.typeFk"
:options="itemTypesOptions"
option-value="id"
option-label="name"
dense
filled
outlined
rounded
use-input
:disable="!selectedCategoryFk"
@update:model-value="
@ -233,8 +234,10 @@ const setCategoryList = (data) => {
:label="t('globals.tag')"
v-model="value.selectedTag"
:options="tagOptions"
option-label="name"
dense
filled
outlined
rounded
:emit-value="false"
use-input
:is-clearable="false"
@ -250,7 +253,8 @@ const setCategoryList = (data) => {
option-value="value"
option-label="value"
dense
filled
outlined
rounded
emit-value
use-input
:disable="!value"
@ -262,6 +266,7 @@ const setCategoryList = (data) => {
v-model="value.value"
:label="t('components.itemsFilterPanel.value')"
:disable="!value"
is-outlined
:is-clearable="false"
@keyup.enter="applyTags(params, searchFn)"
/>
@ -323,6 +328,7 @@ en:
active: Is active
visible: Is visible
floramondo: Is floramondo
salesPersonFk: Buyer
categoryFk: Category
es:
@ -333,6 +339,7 @@ es:
active: Activo
visible: Visible
floramondo: Floramondo
salesPersonFk: Comprador
categoryFk: Categoría
Plant: Planta natural
Flower: Flor fresca

View File

@ -77,7 +77,6 @@ watch(
function findMatches(search, item) {
const matches = [];
function findRoute(search, item) {
if (!item?.children) return;
for (const child of item.children) {
if (search?.indexOf(child.name) > -1) {
matches.push(child);
@ -93,7 +92,7 @@ function findMatches(search, item) {
}
function addChildren(module, route, parent) {
const menus = route?.meta?.menu;
const menus = route?.meta?.menu ?? route?.menus?.[props.source]; //backwards compatible
if (!menus) return;
const matches = findMatches(menus, route);
@ -108,7 +107,11 @@ function getRoutes() {
main: getMainRoutes,
card: getCardRoutes,
};
handleRoutes[props.source]();
try {
handleRoutes[props.source]();
} catch (error) {
throw new Error(`Method is not defined`);
}
}
function getMainRoutes() {
const modules = Object.assign([], navigation.getModules().value);
@ -119,6 +122,7 @@ function getMainRoutes() {
);
if (!moduleDef) continue;
item.children = [];
addChildren(item.module, moduleDef, item.children);
}
@ -128,18 +132,23 @@ function getMainRoutes() {
function getCardRoutes() {
const currentRoute = route.matched[1];
const currentModule = toLowerCamel(currentRoute.name);
let moduleDef;
let index = route.matched.length - 1;
while (!moduleDef && index > 0) {
if (route.matched[index]?.meta?.menu) moduleDef = route.matched[index];
index--;
}
let moduleDef = routes.find((route) => toLowerCamel(route.name) === currentModule);
if (!moduleDef) return;
if (!moduleDef?.menus) moduleDef = betaGetRoutes();
addChildren(currentModule, moduleDef, items.value);
}
function betaGetRoutes() {
let menuRoute;
let index = route.matched.length - 1;
while (!menuRoute && index > 0) {
if (route.matched[index]?.meta?.menu) menuRoute = route.matched[index];
index--;
}
return menuRoute;
}
async function togglePinned(item, event) {
if (event.defaultPrevented) return;
event.preventDefault();

View File

@ -26,7 +26,6 @@ const itemComputed = computed(() => {
:to="{ name: itemComputed.name }"
clickable
v-ripple
:data-cy="`${itemComputed.name}-menu-item`"
>
<QItemSection avatar v-if="itemComputed.icon">
<QIcon :name="itemComputed.icon" />

View File

@ -1,5 +1,5 @@
<script setup>
import { onMounted, ref, computed } from 'vue';
import { onMounted, ref } from 'vue';
import { useI18n } from 'vue-i18n';
import { useState } from 'src/composables/useState';
import { useStateStore } from 'stores/useStateStore';
@ -18,14 +18,6 @@ const state = useState();
const user = state.getUser();
const appName = 'Lilium';
const pinnedModulesRef = ref();
const hostname = window.location.hostname;
const env = ref();
const getEnvironment = computed(() => {
env.value = hostname.split('-');
if (env.value.length <= 1) return;
return env.value[0];
});
onMounted(() => stateStore.setMounted());
const refresh = () => window.location.reload();
@ -57,9 +49,6 @@ const refresh = () => window.location.reload();
{{ t('globals.backToDashboard') }}
</QTooltip>
</QBtn>
<QBadge v-if="getEnvironment" color="primary" align="top">
{{ getEnvironment }}
</QBadge>
</RouterLink>
<VnBreadcrumbs v-if="$q.screen.gt.sm" />
<QSpinner
@ -68,7 +57,7 @@ const refresh = () => window.location.reload();
:class="{
'no-visible': !stateQuery.isLoading().value,
}"
size="sm"
size="xs"
data-cy="loading-spinner"
/>
<QSpace />
@ -96,15 +85,7 @@ const refresh = () => window.location.reload();
</QTooltip>
<PinnedModules ref="pinnedModulesRef" />
</QBtn>
<QBtn
class="q-pa-none"
rounded
dense
flat
no-wrap
id="user"
data-cy="userPanel_btn"
>
<QBtn class="q-pa-none" rounded dense flat no-wrap id="user">
<VnAvatar
:worker-id="user.id"
:title="user.name"

View File

@ -9,7 +9,6 @@ import VnSelect from 'components/common/VnSelect.vue';
import FormPopup from './FormPopup.vue';
import axios from 'axios';
import useNotify from 'src/composables/useNotify.js';
import VnCheckbox from 'src/components/common/VnCheckbox.vue';
const $props = defineProps({
invoiceOutData: {
@ -132,11 +131,15 @@ const refund = async () => {
:required="true"
/> </VnRow
><VnRow>
<VnCheckbox
v-model="invoiceParams.inheritWarehouse"
:label="t('Inherit warehouse')"
:info="t('Inherit warehouse tooltip')"
/>
<div>
<QCheckbox
:label="t('Inherit warehouse')"
v-model="invoiceParams.inheritWarehouse"
/>
<QIcon name="info" class="cursor-info q-ml-sm" size="sm">
<QTooltip>{{ t('Inherit warehouse tooltip') }}</QTooltip>
</QIcon>
</div>
</VnRow>
</template>
</FormPopup>

View File

@ -1,42 +1,16 @@
<script setup>
import { toCurrency } from 'src/filters';
defineProps({ row: { type: Object, required: true } });
</script>
<template>
<span class="q-gutter-x-xs">
<router-link
v-if="row.claim?.claimFk"
:to="{ name: 'ClaimBasicData', params: { id: row.claim?.claimFk } }"
class="link"
>
<QIcon name="vn:claims" size="xs">
<QTooltip>
{{ $t('ticketSale.claim') }}:
{{ row.claim?.claimFk }}
</QTooltip>
</QIcon>
</router-link>
<QIcon
v-if="row?.isDeleted"
color="primary"
name="vn:deletedTicket"
size="xs"
data-cy="ticketDeletedIcon"
>
<QTooltip>
{{ t('Ticket deleted') }}
</QTooltip>
</QIcon>
<QIcon
v-if="row?.hasRisk"
v-if="row?.risk"
name="vn:risk"
:color="row.hasHighRisk ? 'negative' : 'primary'"
size="xs"
>
<QTooltip>
{{ $t('salesTicketsTable.risk') }}:
{{ toCurrency(row.risk - (row.credit ?? 0)) }}
{{ $t('salesTicketsTable.risk') }}: {{ row.risk - row.credit }}
</QTooltip>
</QIcon>
<QIcon
@ -78,7 +52,12 @@ defineProps({ row: { type: Object, required: true } });
>
<QTooltip>{{ $t('salesTicketsTable.purchaseRequest') }}</QTooltip>
</QIcon>
<QIcon v-if="row?.isTaxDataChecked" name="vn:no036" color="primary" size="xs">
<QIcon
v-if="!row?.isTaxDataChecked === 0"
name="vn:no036"
color="primary"
size="xs"
>
<QTooltip>{{ $t('salesTicketsTable.noVerifiedData') }}</QTooltip>
</QIcon>
<QIcon v-if="row?.isFreezed" name="vn:frozen" color="primary" size="xs">

View File

@ -10,7 +10,6 @@ import VnSelect from 'components/common/VnSelect.vue';
import FormPopup from './FormPopup.vue';
import axios from 'axios';
import useNotify from 'src/composables/useNotify.js';
import VnCheckbox from './common/VnCheckbox.vue';
const $props = defineProps({
invoiceOutData: {
@ -87,7 +86,7 @@ const makeInvoice = async () => {
(data) => (
(rectificativeTypeOptions = data),
(transferInvoiceParams.cplusRectificationTypeFk = data.filter(
(type) => type.description == 'I Por diferencias',
(type) => type.description == 'I Por diferencias'
)[0].id)
)
"
@ -100,7 +99,7 @@ const makeInvoice = async () => {
(data) => (
(siiTypeInvoiceOutsOptions = data),
(transferInvoiceParams.siiTypeInvoiceOutFk = data.filter(
(type) => type.code == 'R4',
(type) => type.code == 'R4'
)[0].id)
)
"
@ -122,6 +121,7 @@ const makeInvoice = async () => {
<VnRow>
<VnSelect
:label="t('Client')"
:options="clientsOptions"
hide-selected
option-label="name"
option-value="id"
@ -186,11 +186,15 @@ const makeInvoice = async () => {
/>
</VnRow>
<VnRow>
<VnCheckbox
v-model="checked"
:label="t('Bill destination client')"
:info="t('transferInvoiceInfo')"
/>
<div>
<QCheckbox
:label="t('Bill destination client')"
v-model="checked"
/>
<QIcon name="info" class="cursor-info q-ml-sm" size="sm">
<QTooltip>{{ t('transferInvoiceInfo') }}</QTooltip>
</QIcon>
</div>
</VnRow>
</template>
</FormPopup>

View File

@ -1,8 +1,9 @@
<script setup>
import { markRaw, computed } from 'vue';
import { QIcon, QToggle } from 'quasar';
import { QIcon, QCheckbox } from 'quasar';
import { dashIfEmpty } from 'src/filters';
/* basic input */
import VnSelect from 'components/common/VnSelect.vue';
import VnSelectCache from 'components/common/VnSelectCache.vue';
import VnInput from 'components/common/VnInput.vue';
@ -11,11 +12,8 @@ import VnInputDate from 'components/common/VnInputDate.vue';
import VnInputTime from 'components/common/VnInputTime.vue';
import VnComponent from 'components/common/VnComponent.vue';
import VnUserLink from 'components/ui/VnUserLink.vue';
import VnSelectEnum from '../common/VnSelectEnum.vue';
import VnCheckbox from '../common/VnCheckbox.vue';
const model = defineModel(undefined, { required: true });
const emit = defineEmits(['blur']);
const $props = defineProps({
column: {
type: Object,
@ -41,22 +39,12 @@ const $props = defineProps({
type: Object,
default: null,
},
autofocus: {
type: Boolean,
default: false,
},
showLabel: {
type: Boolean,
default: null,
},
eventHandlers: {
type: Object,
default: null,
},
});
const label = $props.showLabel && $props.column.label ? $props.column.label : '';
const defaultSelect = {
attrs: {
row: $props.row,
@ -64,7 +52,7 @@ const defaultSelect = {
class: 'fit',
},
forceAttrs: {
label,
label: $props.showLabel && $props.column.label,
},
};
@ -76,7 +64,7 @@ const defaultComponents = {
class: 'fit',
},
forceAttrs: {
label,
label: $props.showLabel && $props.column.label,
},
},
number: {
@ -86,7 +74,7 @@ const defaultComponents = {
class: 'fit',
},
forceAttrs: {
label,
label: $props.showLabel && $props.column.label,
},
},
date: {
@ -98,7 +86,7 @@ const defaultComponents = {
class: 'fit',
},
forceAttrs: {
label,
label: $props.showLabel && $props.column.label,
},
},
time: {
@ -107,12 +95,11 @@ const defaultComponents = {
disable: !$props.isEditable,
},
forceAttrs: {
label,
label: $props.showLabel && $props.column.label,
},
},
checkbox: {
ref: 'checkbox',
component: markRaw(VnCheckbox),
component: markRaw(QCheckbox),
attrs: ({ model }) => {
const defaultAttrs = {
disable: !$props.isEditable,
@ -127,11 +114,7 @@ const defaultComponents = {
return defaultAttrs;
},
forceAttrs: {
label,
autofocus: true,
},
events: {
blur: () => emit('blur'),
label: $props.showLabel && $props.column.label,
},
},
select: {
@ -142,19 +125,12 @@ const defaultComponents = {
component: markRaw(VnSelect),
...defaultSelect,
},
selectEnum: {
component: markRaw(VnSelectEnum),
...defaultSelect,
},
icon: {
component: markRaw(QIcon),
},
userLink: {
component: markRaw(VnUserLink),
},
toggle: {
component: markRaw(QToggle),
},
};
const value = computed(() => {
@ -184,28 +160,7 @@ const col = computed(() => {
return newColumn;
});
const components = computed(() => {
const sourceComponents = $props.components ?? defaultComponents;
return Object.keys(sourceComponents).reduce((acc, key) => {
const component = sourceComponents[key];
if (!component || typeof component !== 'object') {
acc[key] = component;
return acc;
}
acc[key] = {
...component,
attrs: {
...(component.attrs || {}),
autofocus: $props.autofocus,
},
event: { ...component?.event, ...$props?.eventHandlers },
};
return acc;
}, {});
});
const components = computed(() => $props.components ?? defaultComponents);
</script>
<template>
<div class="row no-wrap">

View File

@ -1,13 +1,14 @@
<script setup>
import { markRaw, computed } from 'vue';
import { QCheckbox, QToggle } from 'quasar';
import { QCheckbox } from 'quasar';
import { useArrayData } from 'composables/useArrayData';
/* basic input */
import VnSelect from 'components/common/VnSelect.vue';
import VnInput from 'components/common/VnInput.vue';
import VnInputDate from 'components/common/VnInputDate.vue';
import VnInputTime from 'components/common/VnInputTime.vue';
import VnCheckbox from 'components/common/VnCheckbox.vue';
import VnColumn from 'components/VnTable/VnColumn.vue';
import VnTableColumn from 'components/VnTable/VnColumn.vue';
const $props = defineProps({
column: {
@ -26,10 +27,6 @@ const $props = defineProps({
type: String,
default: 'table',
},
customClass: {
type: String,
default: '',
},
});
defineExpose({ addFilter, props: $props });
@ -37,7 +34,7 @@ defineExpose({ addFilter, props: $props });
const model = defineModel(undefined, { required: true });
const arrayData = useArrayData(
$props.dataKey,
$props.searchUrl ? { searchUrl: $props.searchUrl } : null,
$props.searchUrl ? { searchUrl: $props.searchUrl } : null
);
const columnFilter = computed(() => $props.column?.columnFilter);
@ -49,18 +46,19 @@ const enterEvent = {
const defaultAttrs = {
filled: !$props.showTitle,
class: 'q-px-xs q-pb-xs q-pt-none fit',
dense: true,
};
const forceAttrs = {
label: $props.showTitle ? '' : (columnFilter.value?.label ?? $props.column.label),
label: $props.showTitle ? '' : columnFilter.value?.label ?? $props.column.label,
};
const selectComponent = {
component: markRaw(VnSelect),
event: updateEvent,
attrs: {
class: `q-pt-none fit ${$props.customClass}`,
class: 'q-px-sm q-pb-xs q-pt-none fit',
dense: true,
filled: !$props.showTitle,
},
@ -92,6 +90,7 @@ const components = {
event: updateEvent,
attrs: {
...defaultAttrs,
style: 'min-width: 150px',
},
forceAttrs,
},
@ -107,27 +106,17 @@ const components = {
},
},
checkbox: {
component: markRaw(VnCheckbox),
component: markRaw(QCheckbox),
event: updateEvent,
attrs: {
class: $props.showTitle ? 'q-py-sm' : 'q-px-md q-py-xs fit',
dense: true,
class: $props.showTitle ? 'q-py-sm q-mt-md' : 'q-px-md q-py-xs fit',
'toggle-indeterminate': true,
size: 'sm',
},
forceAttrs,
},
select: selectComponent,
rawSelect: selectComponent,
toggle: {
component: markRaw(QToggle),
event: updateEvent,
attrs: {
class: $props.showTitle ? 'q-py-sm' : 'q-px-md q-py-xs fit',
'toggle-indeterminate': true,
size: 'sm',
},
forceAttrs,
},
};
async function addFilter(value, name) {
@ -143,8 +132,19 @@ async function addFilter(value, name) {
await arrayData.addFilter({ params: { [field]: value } });
}
function alignRow() {
switch ($props.column.align) {
case 'left':
return 'justify-start items-start';
case 'right':
return 'justify-end items-end';
default:
return 'flex-center';
}
}
const showFilter = computed(
() => $props.column?.columnFilter !== false && $props.column.name != 'tableActions',
() => $props.column?.columnFilter !== false && $props.column.name != 'tableActions'
);
const onTabPressed = async () => {
@ -152,8 +152,13 @@ const onTabPressed = async () => {
};
</script>
<template>
<div v-if="showFilter" class="full-width" style="overflow: hidden">
<VnColumn
<div
v-if="showFilter"
class="full-width"
:class="alignRow()"
style="max-height: 45px; overflow: hidden"
>
<VnTableColumn
:column="$props.column"
default="input"
v-model="model"
@ -163,8 +168,3 @@ const onTabPressed = async () => {
/>
</div>
</template>
<style lang="scss" scoped>
label.vn-label-padding > .q-field__inner > .q-field__control {
padding: inherit !important;
}
</style>

View File

@ -23,10 +23,6 @@ const $props = defineProps({
type: Boolean,
default: false,
},
align: {
type: String,
default: 'end',
},
});
const hover = ref();
const arrayData = useArrayData($props.dataKey, { searchUrl: $props.searchUrl });
@ -45,78 +41,55 @@ async function orderBy(name, direction) {
break;
}
if (!direction) return await arrayData.deleteOrder(name);
await arrayData.addOrder(name, direction);
}
defineExpose({ orderBy });
function textAlignToFlex(textAlign) {
return `justify-content: ${
{
'text-center': 'center',
'text-left': 'start',
'text-right': 'end',
}[textAlign] || 'start'
};`;
}
</script>
<template>
<div
@mouseenter="hover = true"
@mouseleave="hover = false"
@click="orderBy(name, model?.direction)"
class="items-center no-wrap cursor-pointer title"
:style="textAlignToFlex(align)"
class="row items-center no-wrap cursor-pointer"
>
<span :title="label">{{ label }}</span>
<div v-if="name && (model?.index || vertical)">
<QChip
:label="!vertical ? model?.index : ''"
:icon="
(model?.index || hover) && !vertical
? model?.direction == 'DESC'
? 'arrow_downward'
: 'arrow_upward'
: undefined
"
:size="vertical ? '' : 'sm'"
:class="[
model?.index ? 'color-vn-text' : 'bg-transparent',
vertical ? 'q-mx-none q-py-lg' : '',
]"
class="no-box-shadow"
:clickable="true"
style="min-width: 40px; max-height: 30px"
<QChip
v-if="name"
:label="!vertical ? model?.index : ''"
:icon="
(model?.index || hover) && !vertical
? model?.direction == 'DESC'
? 'arrow_downward'
: 'arrow_upward'
: undefined
"
:size="vertical ? '' : 'sm'"
:class="[
model?.index ? 'color-vn-text' : 'bg-transparent',
vertical ? 'q-px-none' : '',
]"
class="no-box-shadow"
:clickable="true"
style="min-width: 40px"
>
<div
class="column flex-center"
v-if="vertical"
:style="!model?.index && 'color: #5d5d5d'"
>
<div
class="column justify-center text-center"
v-if="vertical"
:style="!model?.index && 'color: #5d5d5d'"
>
{{ model?.index }}
<QIcon
:name="
model?.index
? model?.direction == 'DESC'
? 'arrow_downward'
: 'arrow_upward'
: 'swap_vert'
"
size="xs"
/>
</div>
</QChip>
</div>
{{ model?.index }}
<QIcon
:name="
model?.index
? model?.direction == 'DESC'
? 'arrow_downward'
: 'arrow_upward'
: 'swap_vert'
"
size="xs"
/>
</div>
</QChip>
</div>
</template>
<style lang="scss" scoped>
.title {
display: flex;
align-items: center;
height: 30px;
width: 100%;
color: var(--vn-label-color);
white-space: nowrap;
}
</style>

File diff suppressed because it is too large Load Diff

View File

@ -26,44 +26,28 @@ function columnName(col) {
}
</script>
<template>
<VnFilterPanel
v-bind="$attrs"
:search-button="true"
:disable-submit-event="true"
:search-url
>
<VnFilterPanel v-bind="$attrs" :search-button="true" :disable-submit-event="true">
<template #body="{ params, orders, searchFn }">
<div
class="container"
class="row no-wrap flex-center"
v-for="col of columns.filter((c) => c.columnFilter ?? true)"
:key="col.id"
>
<div class="filter">
<slot
:name="`filter-${col.name}`"
:params="params"
:column-name="columnName(col)"
:search-fn
>
<VnFilter
ref="tableFilterRef"
:column="col"
:data-key="$attrs['data-key']"
v-model="params[columnName(col)]"
:search-url="searchUrl"
/>
</slot>
</div>
<div class="order">
<VnTableOrder
v-if="col?.columnFilter !== false && col?.name !== 'tableActions'"
v-model="orders[col.orderBy ?? col.name]"
:name="col.orderBy ?? col.name"
:data-key="$attrs['data-key']"
:search-url="searchUrl"
:vertical="true"
/>
</div>
<VnFilter
ref="tableFilterRef"
:column="col"
:data-key="$attrs['data-key']"
v-model="params[columnName(col)]"
:search-url="searchUrl"
/>
<VnTableOrder
v-if="col?.columnFilter !== false && col?.name !== 'tableActions'"
v-model="orders[col.orderBy ?? col.name]"
:name="col.orderBy ?? col.name"
:data-key="$attrs['data-key']"
:search-url="searchUrl"
:vertical="true"
/>
</div>
<slot
name="moreFilterPanel"
@ -84,21 +68,3 @@ function columnName(col) {
</template>
</VnFilterPanel>
</template>
<style lang="scss" scoped>
.container {
display: flex;
justify-content: center;
align-items: center;
min-height: 45px;
gap: 10px;
}
.filter {
width: 70%;
min-height: 40px;
text-align: center;
}
.order {
width: 10%;
}
</style>

View File

@ -32,21 +32,16 @@ const areAllChecksMarked = computed(() => {
function setUserConfigViewData(data, isLocal) {
if (!data) return;
// Importante: El name de las columnas de la tabla debe conincidir con el name de las variables que devuelve la view config
if (!isLocal) localColumns.value = [];
// Array to Object
const skippeds = $props.skip.reduce((a, v) => ({ ...a, [v]: v }), {});
for (let column of columns.value) {
const { label, name, labelAbbreviation } = column;
const { label, name } = column;
if (skippeds[name]) continue;
column.visible = data[name] ?? true;
if (!isLocal)
localColumns.value.push({
name,
label,
labelAbbreviation,
visible: column.visible,
});
if (!isLocal) localColumns.value.push({ name, label, visible: column.visible });
}
}
@ -157,11 +152,7 @@ onMounted(async () => {
<QCheckbox
v-for="col in localColumns"
:key="col.name"
:label="
col?.labelAbbreviation
? col.labelAbbreviation + ` (${col.label ?? col.name})`
: (col.label ?? col.name)
"
:label="col.label ?? col.name"
v-model="col.visible"
/>
</div>

View File

@ -27,58 +27,30 @@ describe('VnTable', () => {
beforeEach(() => (vm.selected = []));
describe('handleSelection()', () => {
const rows = [
{ $index: 0 },
{ $index: 1 },
{ $index: 2 },
{ $index: 3 },
{ $index: 4 },
];
it('should add rows to selected when shift key is pressed and rows are added in ascending order', () => {
const selectedRows = [{ $index: 1 }];
const rows = [{ $index: 0 }, { $index: 1 }, { $index: 2 }];
const selectedRows = [{ $index: 1 }];
it('should add rows to selected when shift key is pressed and rows are added except last one', () => {
vm.handleSelection(
{ evt: { shiftKey: true }, added: true, rows: selectedRows },
rows,
rows
);
expect(vm.selected).toEqual([{ $index: 0 }]);
});
it('should add rows to selected when shift key is pressed and rows are added in descending order', () => {
const selectedRows = [{ $index: 3 }];
vm.handleSelection(
{ evt: { shiftKey: true }, added: true, rows: selectedRows },
rows,
);
expect(vm.selected).toEqual([{ $index: 0 }, { $index: 1 }, { $index: 2 }]);
});
it('should not add rows to selected when shift key is not pressed', () => {
const selectedRows = [{ $index: 1 }];
vm.handleSelection(
{ evt: { shiftKey: false }, added: true, rows: selectedRows },
rows,
rows
);
expect(vm.selected).toEqual([]);
});
it('should not add rows to selected when rows are not added', () => {
const selectedRows = [{ $index: 1 }];
vm.handleSelection(
{ evt: { shiftKey: true }, added: false, rows: selectedRows },
rows,
rows
);
expect(vm.selected).toEqual([]);
});
it('should add all rows between the smallest and largest selected indexes', () => {
vm.selected = [{ $index: 1 }, { $index: 3 }];
const selectedRows = [{ $index: 4 }];
vm.handleSelection(
{ evt: { shiftKey: true }, added: true, rows: selectedRows },
rows,
);
expect(vm.selected).toEqual([{ $index: 1 }, { $index: 3 }, { $index: 2 }]);
});
});
});

View File

@ -30,8 +30,8 @@ describe('CrudModel', () => {
saveFn: '',
},
});
wrapper = wrapper.wrapper;
vm = wrapper.vm;
wrapper=wrapper.wrapper;
vm=wrapper.vm;
});
beforeEach(() => {
@ -143,14 +143,14 @@ describe('CrudModel', () => {
});
it('should return true if object is empty', async () => {
dummyObj = {};
result = vm.isEmpty(dummyObj);
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 };
dummyObj = {a:1, b:2, c:3};
result = vm.isEmpty(dummyObj);
expect(result).toBe(false);
@ -158,31 +158,29 @@ describe('CrudModel', () => {
it('should return true if array is empty', async () => {
dummyArray = [];
result = vm.isEmpty(dummyArray);
result = vm.isEmpty(dummyArray);
expect(result).toBe(true);
});
it('should return false if array is not empty', async () => {
dummyArray = [1, 2, 3];
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,
},
];
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);
@ -202,7 +200,7 @@ describe('CrudModel', () => {
lastName: 'Stark',
age: 42,
};
vm.resetData(data);
expect(vm.originalData).toEqual(data);
@ -212,19 +210,17 @@ describe('CrudModel', () => {
});
describe('saveChanges()', () => {
data = [
{
name: 'Tony',
lastName: 'Stark',
age: 42,
},
];
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);
@ -233,15 +229,13 @@ describe('CrudModel', () => {
});
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,
},
];
const postMock =vi.spyOn(axios, 'post');
vm.formData = [{
name: 'Bruce',
lastName: 'Wayne',
age: 45,
}]
await vm.saveChanges(data);

View File

@ -57,7 +57,6 @@ describe('FormModel', () => {
vm.state.set(model, formInitialData);
expect(vm.hasChanges).toBe(false);
await vm.$nextTick();
vm.formData.mockKey = 'newVal';
await vm.$nextTick();
expect(vm.hasChanges).toBe(true);
@ -95,12 +94,8 @@ describe('FormModel', () => {
it('should call axios.patch with the right data', async () => {
const spy = vi.spyOn(axios, 'patch').mockResolvedValue({ data: {} });
const { vm } = mount({ propsData: { url, model } });
vm.formData = {};
vm.formData.mockKey = 'newVal';
await vm.$nextTick();
vm.formData = { mockKey: 'newVal' };
await vm.$nextTick();
await vm.save();
expect(spy).toHaveBeenCalled();
vm.formData.mockKey = 'mockVal';

View File

@ -15,7 +15,10 @@ vi.mock('src/router/modules', () => ({
meta: {
title: 'customers',
icon: 'vn:client',
menu: ['CustomerList', 'CustomerCreate'],
},
menus: {
main: ['CustomerList', 'CustomerCreate'],
card: ['CustomerBasicData'],
},
children: [
{
@ -47,6 +50,14 @@ vi.mock('src/router/modules', () => ({
],
},
},
{
path: 'create',
name: 'CustomerCreate',
meta: {
title: 'createCustomer',
icon: 'vn:addperson',
},
},
],
},
],
@ -87,7 +98,7 @@ vi.spyOn(vueRouter, 'useRoute').mockReturnValue({
icon: 'vn:client',
moduleName: 'Customer',
keyBinding: 'c',
menu: ['customer'],
menu: 'customer',
},
},
],
@ -249,6 +260,15 @@ describe('Leftmenu as main', () => {
});
});
it('should handle a single matched route with a menu', () => {
const route = {
matched: [{ meta: { menu: 'customer' } }],
};
const result = vm.betaGetRoutes();
expect(result.meta.menu).toEqual(route.matched[0].meta.menu);
});
it('should get routes for main source', () => {
vm.props.source = 'main';
vm.getRoutes();
@ -331,9 +351,8 @@ describe('addChildren', () => {
it('should handle routes with no meta menu', () => {
const route = {
meta: {
menu: [],
},
meta: {},
menus: {},
};
const parent = [];

View File

@ -1,65 +1,61 @@
import { vi, describe, expect, it, beforeEach, afterEach } from 'vitest';
import { vi, describe, expect, it, beforeEach, beforeAll, afterEach } from 'vitest';
import { createWrapper } from 'app/test/vitest/helper';
import UserPanel from 'src/components/UserPanel.vue';
import axios from 'axios';
import { useState } from 'src/composables/useState';
vi.mock('src/utils/quasarLang', () => ({
default: vi.fn(),
}));
describe('UserPanel', () => {
let wrapper;
let vm;
let state;
let wrapper;
let vm;
let state;
beforeEach(() => {
wrapper = createWrapper(UserPanel, {});
state = useState();
state.setUser({
id: 115,
name: 'itmanagement',
nickname: 'itManagementNick',
lang: 'en',
darkMode: false,
companyFk: 442,
warehouseFk: 1,
beforeEach(() => {
wrapper = createWrapper(UserPanel, {});
state = useState();
state.setUser({
id: 115,
name: 'itmanagement',
nickname: 'itManagementNick',
lang: 'en',
darkMode: false,
companyFk: 442,
warehouseFk: 1,
});
wrapper = wrapper.wrapper;
vm = wrapper.vm;
});
wrapper = wrapper.wrapper;
vm = wrapper.vm;
});
afterEach(() => {
vi.clearAllMocks();
});
afterEach(() => {
vi.clearAllMocks();
});
it('should fetch warehouses data on mounted', async () => {
const fetchData = wrapper.findComponent({ name: 'FetchData' });
expect(fetchData.props('url')).toBe('Warehouses');
expect(fetchData.props('autoLoad')).toBe(true);
});
it('should fetch warehouses data on mounted', async () => {
const fetchData = wrapper.findComponent({ name: 'FetchData' });
expect(fetchData.props('url')).toBe('Warehouses');
expect(fetchData.props('autoLoad')).toBe(true);
});
it('should toggle dark mode correctly and update preferences', async () => {
await vm.saveDarkMode(true);
expect(axios.patch).toHaveBeenCalledWith('/UserConfigs/115', { darkMode: true });
expect(vm.user.darkMode).toBe(true);
await vm.updatePreferences();
expect(vm.darkMode).toBe(true);
});
it('should toggle dark mode correctly and update preferences', async () => {
await vm.saveDarkMode(true);
expect(axios.patch).toHaveBeenCalledWith('/UserConfigs/115', { darkMode: true });
expect(vm.user.darkMode).toBe(true);
vm.updatePreferences();
expect(vm.darkMode).toBe(true);
});
it('should change user language and update preferences', async () => {
const userLanguage = 'es';
await vm.saveLanguage(userLanguage);
expect(axios.patch).toHaveBeenCalledWith('/VnUsers/115', { lang: userLanguage });
expect(vm.user.lang).toBe(userLanguage);
await vm.updatePreferences();
expect(vm.locale).toBe(userLanguage);
});
it('should change user language and update preferences', async () => {
const userLanguage = 'es';
await vm.saveLanguage(userLanguage);
expect(axios.patch).toHaveBeenCalledWith('/VnUsers/115', { lang: userLanguage });
expect(vm.user.lang).toBe(userLanguage);
vm.updatePreferences();
expect(vm.locale).toBe(userLanguage);
});
it('should update user data', async () => {
const key = 'name';
const value = 'itboss';
await vm.saveUserData(key, value);
expect(axios.post).toHaveBeenCalledWith('UserConfigs/setUserConfig', { [key]: value });
});
});
it('should update user data', async () => {
const key = 'name';
const value = 'itboss';
await vm.saveUserData(key, value);
expect(axios.post).toHaveBeenCalledWith('UserConfigs/setUserConfig', { [key]: value });
});
});

View File

@ -11,13 +11,6 @@ const stateStore = useStateStore();
const slots = useSlots();
const hasContent = useHasContent('#right-panel');
defineProps({
overlay: {
type: Boolean,
default: false,
},
});
onMounted(() => {
if ((!slots['right-panel'] && !hasContent.value) || quasar.platform.is.mobile)
stateStore.rightDrawer = false;
@ -41,12 +34,7 @@ onMounted(() => {
</QBtn>
</div>
</Teleport>
<QDrawer
v-model="stateStore.rightDrawer"
side="right"
:width="256"
:overlay="overlay"
>
<QDrawer v-model="stateStore.rightDrawer" side="right" :width="256">
<QScrollArea class="fit">
<div id="right-panel"></div>
<slot v-if="!hasContent" name="right-panel" />

View File

@ -56,12 +56,7 @@ async function confirm() {
{{ t('The notification will be sent to the following address') }}
</QCardSection>
<QCardSection class="q-pt-none">
<VnInput
v-model="address"
is-outlined
autofocus
data-cy="SendEmailNotifiactionDialogInput"
/>
<VnInput v-model="address" is-outlined autofocus />
</QCardSection>
<QCardActions align="right">
<QBtn :label="t('globals.cancel')" color="primary" flat v-close-popup />

View File

@ -1,15 +1,15 @@
<script setup>
import { useDialogPluginComponent } from 'quasar';
import { useI18n } from 'vue-i18n';
import { computed, ref } from 'vue';
import {useDialogPluginComponent} from 'quasar';
import {useI18n} from 'vue-i18n';
import {computed, ref} from 'vue';
import VnInput from 'components/common/VnInput.vue';
import axios from 'axios';
import useNotify from 'composables/useNotify';
import useNotify from "composables/useNotify";
const MESSAGE_MAX_LENGTH = 160;
const { t } = useI18n();
const { notify } = useNotify();
const {t} = useI18n();
const {notify} = useNotify();
const props = defineProps({
title: {
type: String,
@ -34,7 +34,7 @@ const props = defineProps({
});
const emit = defineEmits([...useDialogPluginComponent.emits, 'sent']);
const { dialogRef, onDialogHide } = useDialogPluginComponent();
const {dialogRef, onDialogHide} = useDialogPluginComponent();
const smsRules = [
(val) => (val && val.length > 0) || t("The message can't be empty"),
@ -43,10 +43,10 @@ const smsRules = [
t("The message it's too long"),
];
const message = ref(t('routeDelay'));
const message = ref('');
const charactersRemaining = computed(
() => MESSAGE_MAX_LENGTH - new Blob([message.value]).size,
() => MESSAGE_MAX_LENGTH - new Blob([message.value]).size
);
const charactersChipColor = computed(() => {
@ -114,7 +114,7 @@ const onSubmit = async () => {
<QTooltip>
{{
t(
'Special characters like accents counts as a multiple',
'Special characters like accents counts as a multiple'
)
}}
</QTooltip>
@ -144,10 +144,7 @@ const onSubmit = async () => {
max-width: 450px;
}
</style>
<i18n>
en:
routeDelay: "Your order has been delayed in transit.\nDelivery will take place throughout the day.\nWe apologize for the inconvenience and appreciate your patience."
es:
Message: Mensaje
Send: Enviar
@ -156,5 +153,4 @@ es:
The destination can't be empty: El destinatario no puede estar vacio
The message can't be empty: El mensaje no puede estar vacio
The message it's too long: El mensaje es demasiado largo
routeDelay: "Retraso en ruta.\nInformamos que la ruta que lleva su pedido ha sufrido un retraso y la entrega se hará a lo largo del día.\nDisculpe las molestias."
</i18n>
</i18n>

View File

@ -1,14 +1,83 @@
<script setup>
import VnInput from './VnInput.vue';
import { useAccountShortToStandard } from 'src/composables/useAccountShortToStandard';
import { nextTick, ref, watch } from 'vue';
import { QInput } from 'quasar';
const model = defineModel({ prop: 'modelValue' });
const $props = defineProps({
modelValue: {
type: String,
default: '',
},
insertable: {
type: Boolean,
default: false,
},
});
const emit = defineEmits(['update:modelValue', 'accountShortToStandard']);
let internalValue = ref($props.modelValue);
watch(
() => $props.modelValue,
(newVal) => {
internalValue.value = newVal;
}
);
watch(
() => internalValue.value,
(newVal) => {
emit('update:modelValue', newVal);
accountShortToStandard();
}
);
const handleKeydown = (e) => {
if (e.key === 'Backspace') return;
if (e.key === '.') {
accountShortToStandard();
// TODO: Fix this setTimeout, with nextTick doesn't work
setTimeout(() => {
setCursorPosition(0, e.target);
}, 1);
return;
}
if ($props.insertable && e.key.match(/[0-9]/)) {
handleInsertMode(e);
}
};
function setCursorPosition(pos, el = vnInputRef.value) {
el.focus();
el.setSelectionRange(pos, pos);
}
const vnInputRef = ref(false);
const handleInsertMode = (e) => {
e.preventDefault();
const input = e.target;
const cursorPos = input.selectionStart;
const { maxlength } = vnInputRef.value;
let currentValue = internalValue.value;
if (!currentValue) currentValue = e.key;
const newValue = e.key;
if (newValue && !isNaN(newValue) && cursorPos < maxlength) {
internalValue.value =
currentValue.substring(0, cursorPos) +
newValue +
currentValue.substring(cursorPos + 1);
}
nextTick(() => {
input.setSelectionRange(cursorPos + 1, cursorPos + 1);
});
};
function accountShortToStandard() {
internalValue.value = internalValue.value?.replace(
'.',
'0'.repeat(11 - internalValue.value.length)
);
}
</script>
<template>
<VnInput
v-model="model"
ref="inputRef"
@keydown.tab="model = useAccountShortToStandard($event.target.value) ?? model"
@input="model = $event.target.value.replace(/[^\d.]/g, '')"
/>
<QInput @keydown="handleKeydown" ref="vnInputRef" v-model="internalValue" />
</template>

View File

@ -1,93 +1,89 @@
<script setup>
import { onBeforeMount, computed, markRaw } from 'vue';
import { useRoute, useRouter, onBeforeRouteUpdate, onBeforeRouteLeave } from 'vue-router';
import { onBeforeMount, computed } from 'vue';
import { useRoute, useRouter, onBeforeRouteUpdate } from 'vue-router';
import { useArrayData } from 'src/composables/useArrayData';
import { useStateStore } from 'stores/useStateStore';
import useCardSize from 'src/composables/useCardSize';
import VnSubToolbar from '../ui/VnSubToolbar.vue';
const emit = defineEmits(['onFetch']);
import VnSearchbar from 'components/ui/VnSearchbar.vue';
import LeftMenu from 'components/LeftMenu.vue';
import RightMenu from 'components/common/RightMenu.vue';
const props = defineProps({
id: { type: Number, required: false, default: null },
dataKey: { type: String, required: true },
url: { type: String, default: undefined },
idInWhere: { type: Boolean, default: false },
filter: { type: Object, default: () => {} },
descriptor: { type: Object, required: true },
filterPanel: { type: Object, default: undefined },
idInWhere: { type: Boolean, default: false },
searchDataKey: { type: String, default: undefined },
searchbarProps: { type: Object, default: undefined },
redirectOnError: { type: Boolean, default: false },
visual: { type: Boolean, default: true },
});
const route = useRoute();
const stateStore = useStateStore();
const route = useRoute();
const router = useRouter();
const entityId = computed(() => props.id || route?.params?.id);
let arrayData = getArrayData(entityId.value, props.url);
const searchRightDataKey = computed(() => {
if (!props.searchDataKey) return route.name;
return props.searchDataKey;
});
onBeforeRouteLeave(() => {
stateStore.cardDescriptorChangeValue(null);
const arrayData = useArrayData(props.dataKey, {
url: props.url,
userFilter: props.filter,
oneRecord: true,
});
onBeforeMount(async () => {
stateStore.cardDescriptorChangeValue(markRaw(props.descriptor));
const route = router.currentRoute.value;
try {
await fetch(entityId.value);
await fetch(route.params.id);
} catch {
const { matched: matches } = route;
const { matched: matches } = router.currentRoute.value;
const { path } = matches.at(-1);
router.push({ path: path.replace(/:id.*/, '') });
}
});
onBeforeRouteUpdate(async (to, from) => {
if (hasRouteParam(to.params)) {
const { matched } = router.currentRoute.value;
const { name } = matched.at(-3);
if (name) {
router.push({ name, params: to.params });
}
}
if (entityId.value !== to.params.id) await fetch(to.params.id, true);
const id = to.params.id;
if (id !== from.params.id) await fetch(id, true);
});
async function fetch(id, append = false) {
if (props.idInWhere) arrayData.store.filter.where = { id };
else {
arrayData = getArrayData(id);
}
await arrayData.fetch({ append, updateRouter: false });
emit('onFetch', arrayData.store.data);
}
function hasRouteParam(params, valueToCheck = ':addressId') {
return Object.values(params).includes(valueToCheck);
}
function formatUrl(id) {
const newId = id || entityId.value;
const regex = /\/(\d+)/;
if (!regex.test(props.url)) return `${props.url}/${newId}`;
return props.url.replace(regex, `/${newId}`);
}
function getArrayData(id, url) {
return useArrayData(props.dataKey, {
url: url ?? formatUrl(id),
userFilter: props.filter,
oneRecord: true,
});
if (props.idInWhere) arrayData.store.filter.where = { id };
else if (!regex.test(props.url)) arrayData.store.url = `${props.url}/${id}`;
else arrayData.store.url = props.url.replace(regex, `/${id}`);
await arrayData.fetch({ append, updateRouter: false });
}
</script>
<template>
<template v-if="visual">
<VnSubToolbar />
<div :class="[useCardSize(), $attrs.class]">
<RouterView :key="$route.path" />
</div>
</template>
<QDrawer
v-model="stateStore.leftDrawer"
show-if-above
:width="256"
v-if="stateStore.isHeaderMounted()"
>
<QScrollArea class="fit">
<component :is="descriptor" />
<QSeparator />
<LeftMenu source="card" />
</QScrollArea>
</QDrawer>
<slot name="searchbar" v-if="props.searchDataKey">
<VnSearchbar :data-key="props.searchDataKey" v-bind="props.searchbarProps" />
</slot>
<RightMenu>
<template #right-panel v-if="props.filterPanel">
<component :is="props.filterPanel" :data-key="searchRightDataKey" />
</template>
</RightMenu>
<QPageContainer>
<QPage>
<VnSubToolbar />
<div :class="[useCardSize(), $attrs.class]">
<RouterView :key="$route.path" />
</div>
</QPage>
</QPageContainer>
</template>

View File

@ -0,0 +1,64 @@
<script setup>
import { onBeforeMount } from 'vue';
import { useRouter, onBeforeRouteUpdate } from 'vue-router';
import { useArrayData } from 'src/composables/useArrayData';
import { useStateStore } from 'stores/useStateStore';
import useCardSize from 'src/composables/useCardSize';
import LeftMenu from 'components/LeftMenu.vue';
import VnSubToolbar from '../ui/VnSubToolbar.vue';
const props = defineProps({
dataKey: { type: String, required: true },
url: { type: String, default: undefined },
idInWhere: { type: Boolean, default: false },
filter: { type: Object, default: () => {} },
descriptor: { type: Object, required: true },
filterPanel: { type: Object, default: undefined },
searchDataKey: { type: String, default: undefined },
searchbarProps: { type: Object, default: undefined },
redirectOnError: { type: Boolean, default: false },
});
const stateStore = useStateStore();
const router = useRouter();
const arrayData = useArrayData(props.dataKey, {
url: props.url,
userFilter: props.filter,
oneRecord: true,
});
onBeforeMount(async () => {
const route = router.currentRoute.value;
try {
await fetch(route.params.id);
} catch {
const { matched: matches } = route;
const { path } = matches.at(-1);
router.push({ path: path.replace(/:id.*/, '') });
}
});
onBeforeRouteUpdate(async (to, from) => {
const id = to.params.id;
if (id !== from.params.id) await fetch(id, true);
});
async function fetch(id, append = false) {
const regex = /\/(\d+)/;
if (props.idInWhere) arrayData.store.filter.where = { id };
else if (!regex.test(props.url)) arrayData.store.url = `${props.url}/${id}`;
else arrayData.store.url = props.url.replace(regex, `/${id}`);
await arrayData.fetch({ append, updateRouter: false });
}
</script>
<template>
<Teleport to="#left-panel" v-if="stateStore.isHeaderMounted()">
<component :is="descriptor" />
<QSeparator />
<LeftMenu source="card" />
</Teleport>
<VnSubToolbar />
<div :class="[useCardSize(), $attrs.class]">
<RouterView :key="$route.path" />
</div>
</template>

View File

@ -1,47 +0,0 @@
<script setup>
import { computed } from 'vue';
const model = defineModel({ type: [Number, Boolean] });
const $props = defineProps({
info: {
type: String,
default: null,
},
});
const checkboxModel = computed({
get() {
if (typeof model.value === 'number') {
return model.value !== 0;
}
return model.value;
},
set(value) {
if (typeof model.value === 'number') {
model.value = value ? 1 : 0;
} else {
model.value = value;
}
},
});
</script>
<template>
<div>
<QCheckbox
v-bind="$attrs"
v-model="checkboxModel"
:data-cy="$attrs['data-cy'] ?? `vnCheckbox${$attrs['label'] ?? ''}`"
/>
<QIcon
v-if="info"
v-bind="$attrs"
class="cursor-info q-ml-sm"
name="info"
size="sm"
>
<QTooltip>
{{ info }}
</QTooltip>
</QIcon>
</div>
</template>

View File

@ -1,32 +0,0 @@
<script setup>
const $props = defineProps({
colors: {
type: String,
default: '{"value": []}',
},
});
const colorArray = JSON.parse($props.colors)?.value;
const maxHeight = 30;
const colorHeight = maxHeight / colorArray?.length;
</script>
<template>
<div v-if="colors" class="color-div" :style="{ height: `${maxHeight}px` }">
<div
v-for="(color, index) in colorArray"
:key="index"
:style="{
backgroundColor: `#${color}`,
height: `${colorHeight}px`,
}"
>
&nbsp;
</div>
</div>
</template>
<style scoped>
.color-div {
display: flex;
flex-direction: column;
}
</style>

View File

@ -17,8 +17,6 @@ const $props = defineProps({
},
});
const emit = defineEmits(['blur']);
const componentArray = computed(() => {
if (typeof $props.prop === 'object') return [$props.prop];
return $props.prop;
@ -48,8 +46,7 @@ function toValueAttrs(attrs) {
<span
v-for="toComponent of componentArray"
:key="toComponent.name"
class="column fit"
:class="toComponent?.component == 'checkbox' ? 'flex-center' : ''"
class="column flex-center fit"
>
<component
v-if="toComponent?.component"
@ -57,7 +54,6 @@ function toValueAttrs(attrs) {
v-bind="mix(toComponent).attrs"
v-on="mix(toComponent).event ?? {}"
v-model="model"
@blur="emit('blur')"
/>
</span>
</template>

View File

@ -35,10 +35,6 @@ const $props = defineProps({
type: String,
default: null,
},
hasFile: {
type: Boolean,
default: false,
},
});
const warehouses = ref();
@ -94,7 +90,6 @@ function defaultData() {
if ($props.formInitialData) return (dms.value = $props.formInitialData);
return addDefaultData({
reference: route.params.id,
hasFile: $props.hasFile,
});
}
@ -182,7 +177,6 @@ function addDefaultData(data) {
name="vn:attach"
class="cursor-pointer"
@click="inputFileRef.pickFiles()"
data-cy="attachFile"
>
<QTooltip>{{ t('globals.selectFile') }}</QTooltip>
</QIcon>

View File

@ -1,166 +0,0 @@
<script setup>
import VnConfirm from '../ui/VnConfirm.vue';
import VnInput from './VnInput.vue';
import VnDms from './VnDms.vue';
import axios from 'axios';
import { useQuasar } from 'quasar';
import { ref } from 'vue';
import { useI18n } from 'vue-i18n';
import { downloadFile } from 'src/composables/downloadFile';
const { t } = useI18n();
const quasar = useQuasar();
const documentDialogRef = ref({});
const editDownloadDisabled = ref(false);
const $props = defineProps({
defaultDmsCode: {
type: String,
default: 'invoiceIn',
},
disable: {
type: Boolean,
default: true,
},
data: {
type: Object,
default: null,
},
formRef: {
type: Object,
default: null,
},
});
function deleteFile(dmsFk) {
quasar
.dialog({
component: VnConfirm,
componentProps: {
title: t('globals.confirmDeletion'),
message: t('globals.confirmDeletionMessage'),
},
})
.onOk(async () => {
await axios.post(`dms/${dmsFk}/removeFile`);
$props.formRef.formData.dmsFk = null;
$props.formRef.formData.dms = undefined;
$props.formRef.hasChanges = true;
$props.formRef.save();
});
}
</script>
<template>
<div class="row no-wrap">
<VnInput
:label="t('Document')"
v-model="data.dmsFk"
clearable
clear-icon="close"
class="full-width"
:disable="disable"
/>
<div
v-if="data.dmsFk"
class="row no-wrap q-pa-xs q-gutter-x-xs"
data-cy="dms-buttons"
>
<QBtn
:disable="editDownloadDisabled"
@click="downloadFile(data.dmsFk)"
icon="cloud_download"
color="primary"
flat
:class="{
'no-pointer-events': editDownloadDisabled,
}"
padding="xs"
round
>
<QTooltip>{{ t('Download file') }}</QTooltip>
</QBtn>
<QBtn
:disable="editDownloadDisabled"
@click="
() => {
documentDialogRef.show = true;
documentDialogRef.dms = data.dms;
}
"
icon="edit"
color="primary"
flat
:class="{
'no-pointer-events': editDownloadDisabled,
}"
padding="xs"
round
>
<QTooltip>{{ t('Edit document') }}</QTooltip>
</QBtn>
<QBtn
:disable="editDownloadDisabled"
@click="deleteFile(data.dmsFk)"
icon="delete"
color="primary"
flat
round
:class="{
'no-pointer-events': editDownloadDisabled,
}"
padding="xs"
>
<QTooltip>{{ t('Delete file') }}</QTooltip>
</QBtn>
</div>
<QBtn
v-else
icon="add_circle"
color="primary"
flat
round
v-shortcut="'+'"
padding="xs"
@click="
() => {
documentDialogRef.show = true;
delete documentDialogRef.dms;
}
"
data-cy="dms-create"
>
<QTooltip>{{ t('Create document') }}</QTooltip>
</QBtn>
</div>
<QDialog v-model="documentDialogRef.show">
<VnDms
model="dms"
:default-dms-code="defaultDmsCode"
:form-initial-data="documentDialogRef.dms"
:url="
documentDialogRef.dms
? `Dms/${documentDialogRef.dms.id}/updateFile`
: 'Dms/uploadFile'
"
:description="documentDialogRef.supplierName"
@on-data-saved="
(_, { data }) => {
let dmsData = data;
if (Array.isArray(data)) dmsData = data[0];
formRef.formData.dmsFk = dmsData.id;
formRef.formData.dms = dmsData;
formRef.hasChanges = true;
formRef.save();
}
"
/>
</QDialog>
</template>
<i18n>
es:
Document: Documento
Download file: Descargar archivo
Edit document: Editar documento
Delete file: Eliminar archivo
Create document: Crear documento
</i18n>

View File

@ -389,7 +389,10 @@ defineExpose({
</div>
</template>
</QTable>
<div v-else class="info-row q-pa-md text-center">
<div
v-else
class="info-row q-pa-md text-center"
>
<h5>
{{ t('No data to display') }}
</h5>
@ -413,7 +416,6 @@ defineExpose({
v-shortcut
@click="showFormDialog()"
class="fill-icon"
data-cy="addButton"
>
<QTooltip>
{{ t('Upload file') }}

View File

@ -1,53 +0,0 @@
<script setup>
import { ref } from 'vue';
import VnSelect from './VnSelect.vue';
const stateBtnDropdownRef = ref();
const emit = defineEmits(['changeState']);
const $props = defineProps({
disable: {
type: Boolean,
default: null,
},
options: {
type: Array,
default: null,
},
optionLabel: {
type: String,
default: 'name',
},
optionValue: {
type: String,
default: 'id',
},
});
async function changeState(value) {
stateBtnDropdownRef.value?.hide();
emit('changeState', value);
}
</script>
<template>
<QBtnDropdown
ref="stateBtnDropdownRef"
color="black"
text-color="white"
:label="$t('globals.changeState')"
:disable="$props.disable"
>
<VnSelect
:options="$props.options"
:option-label="$props.optionLabel"
:option-value="$props.optionValue"
hide-selected
hide-dropdown-icon
focus-on-mount
@update:model-value="changeState"
>
</VnSelect>
</QBtnDropdown>
</template>

View File

@ -11,7 +11,6 @@ const emit = defineEmits([
'update:options',
'keyup.enter',
'remove',
'blur',
]);
const $props = defineProps({
@ -83,8 +82,8 @@ const mixinRules = [
requiredFieldRule,
...($attrs.rules ?? []),
(val) => {
const maxlength = $props.maxlength;
if (maxlength && +val?.length > maxlength)
const { maxlength } = vnInputRef.value;
if (maxlength && +val.length > maxlength)
return t(`maxLength`, { value: maxlength });
const { min, max } = vnInputRef.value.$attrs;
if (!min) return null;
@ -108,7 +107,7 @@ const handleInsertMode = (e) => {
e.preventDefault();
const input = e.target;
const cursorPos = input.selectionStart;
const maxlength = $props.maxlength;
const { maxlength } = vnInputRef.value;
let currentValue = value.value;
if (!currentValue) currentValue = e.key;
const newValue = e.key;
@ -137,15 +136,14 @@ const handleUppercase = () => {
:type="$attrs.type"
:class="{ required: isRequired }"
@keyup.enter="emit('keyup.enter')"
@blur="emit('blur')"
@keydown="handleKeydown"
:clearable="false"
:rules="mixinRules"
:lazy-rules="true"
hide-bottom-space
:data-cy="($attrs['data-cy'] ?? $attrs.label) + '_input'"
:data-cy="$attrs.dataCy ?? $attrs.label + '_input'"
>
<template #prepend v-if="$slots.prepend">
<template #prepend>
<slot name="prepend" />
</template>
<template #append>
@ -170,11 +168,11 @@ const handleUppercase = () => {
}
"
></QIcon>
<QIcon
name="match_case"
size="xs"
v-if="!$attrs.disabled && !$attrs.readonly && $props.uppercase"
v-if="!$attrs.disabled && !($attrs.readonly) && $props.uppercase"
@click="handleUppercase"
class="uppercase-icon"
>
@ -182,7 +180,7 @@ const handleUppercase = () => {
{{ t('Convert to uppercase') }}
</QTooltip>
</QIcon>
<slot name="append" v-if="$slots.append && !$attrs.disabled" />
<QIcon v-if="info" name="info">
<QTooltip max-width="350px">
@ -196,15 +194,13 @@ const handleUppercase = () => {
<style>
.uppercase-icon {
transition:
color 0.3s,
transform 0.2s;
cursor: pointer;
transition: color 0.3s, transform 0.2s;
cursor: pointer;
}
.uppercase-icon:hover {
color: #ed9937;
transform: scale(1.2);
color: #ed9937;
transform: scale(1.2);
}
</style>
<i18n>
@ -218,4 +214,4 @@ const handleUppercase = () => {
maxLength: El valor excede los {value} carácteres
inputMax: Debe ser menor a {value}
Convert to uppercase: Convertir a mayúsculas
</i18n>
</i18n>

View File

@ -42,7 +42,7 @@ const formattedDate = computed({
if (value.at(2) == '/') value = value.split('/').reverse().join('/');
value = date.formatDate(
new Date(value).toISOString(),
'YYYY-MM-DDTHH:mm:ss.SSSZ',
'YYYY-MM-DDTHH:mm:ss.SSSZ'
);
}
const [year, month, day] = value.split('-').map((e) => parseInt(e));
@ -55,7 +55,7 @@ const formattedDate = computed({
orgDate.getHours(),
orgDate.getMinutes(),
orgDate.getSeconds(),
orgDate.getMilliseconds(),
orgDate.getMilliseconds()
);
}
}
@ -64,7 +64,7 @@ const formattedDate = computed({
});
const popupDate = computed(() =>
model.value ? date.formatDate(new Date(model.value), 'YYYY/MM/DD') : model.value,
model.value ? date.formatDate(new Date(model.value), 'YYYY/MM/DD') : model.value
);
onMounted(() => {
// fix quasar bug
@ -73,7 +73,7 @@ onMounted(() => {
watch(
() => model.value,
(val) => (formattedDate.value = val),
{ immediate: true },
{ immediate: true }
);
const styleAttrs = computed(() => {
@ -107,7 +107,6 @@ const manageDate = (date) => {
@click="isPopupOpen = !isPopupOpen"
@keydown="isPopupOpen = false"
hide-bottom-space
:data-cy="($attrs['data-cy'] ?? $attrs.label) + '_inputDate'"
>
<template #append>
<QIcon

View File

@ -8,7 +8,6 @@ defineProps({
});
const model = defineModel({ type: [Number, String] });
const emit = defineEmits(['blur']);
</script>
<template>
<VnInput
@ -25,6 +24,5 @@ const emit = defineEmits(['blur']);
model = parseFloat(val).toFixed(decimalPlaces);
}
"
@blur="emit('blur')"
/>
</template>

View File

@ -85,7 +85,6 @@ const handleModelValue = (data) => {
:tooltip="t('Create new location')"
:rules="mixinRules"
:lazy-rules="true"
required
>
<template #form>
<CreateNewPostcode

View File

@ -1,5 +1,5 @@
<script setup>
import { ref, onMounted, onUnmounted, watch, computed } from 'vue';
import { ref, onUnmounted, watch } from 'vue';
import { useI18n } from 'vue-i18n';
import { useRoute, useRouter } from 'vue-router';
import axios from 'axios';
@ -10,12 +10,12 @@ import { useColor } from 'src/composables/useColor';
import { useCapitalize } from 'src/composables/useCapitalize';
import { useValidator } from 'src/composables/useValidator';
import VnAvatar from '../ui/VnAvatar.vue';
import VnLogValue from './VnLogValue.vue';
import VnJsonValue from '../common/VnJsonValue.vue';
import FetchData from '../FetchData.vue';
import VnSelect from './VnSelect.vue';
import VnUserLink from '../ui/VnUserLink.vue';
import VnPaginate from '../ui/VnPaginate.vue';
import VnLogFilter from 'src/components/common/VnLogFilter.vue';
import RightMenu from './RightMenu.vue';
import { useFilterParams } from 'src/composables/useFilterParams';
const stateStore = useStateStore();
const validationsStore = useValidator();
@ -72,8 +72,39 @@ const filter = {
};
const paginate = ref();
const dataKey = computed(() => `${props.model}Log`);
const userParams = ref(useFilterParams(dataKey.value).params);
const actions = ref();
const changeInput = ref();
const searchInput = ref();
const userRadio = ref();
const userSelect = ref();
const dateFrom = ref();
const dateFromDialog = ref(false);
const dateTo = ref();
const dateToDialog = ref(false);
const selectedFilters = ref({});
const userTypes = [
{ label: 'All', value: undefined },
{ label: 'User', value: { neq: null } },
{ label: 'System', value: null },
];
const checkboxOptions = ref({
insert: {
label: 'Creates',
selected: false,
},
update: {
label: 'Edits',
selected: false,
},
delete: {
label: 'Deletes',
selected: false,
},
select: {
label: 'Accesses',
selected: false,
},
});
let validations = models;
let pointRecord = ref(null);
@ -215,55 +246,131 @@ async function setLogTree(data) {
function filterByRecord(modelLog) {
byRecord.value = true;
const { id, model } = modelLog;
applyFilter({ changedModelId: id, changedModel: model });
searchInput.value = id;
selectedFilters.value.changedModelId = id;
selectedFilters.value.changedModel = model;
applyFilter();
}
async function applyFilter(params = {}) {
paginate.value.arrayData.resetPagination();
paginate.value.arrayData.applyFilter({
filter: {},
params: { originFk: route.params.id, ...params },
});
async function applyFilter() {
filter.where = { and: [] };
if (
!selectedFilters.value.changedModel ||
(!selectedFilters.value.changedModelValue &&
!selectedFilters.value.changedModelId)
)
byRecord.value = false;
if (!byRecord.value) filter.where.and.push({ originFk: route.params.id });
if (Object.keys(selectedFilters.value).length) {
filter.where.and.push(selectedFilters.value);
}
paginate.value.fetch({ filter });
}
function exprBuilder(param, value) {
switch (param) {
case 'changedModelValue':
return { [param]: { like: `%${value}%` } };
case 'change':
if (value)
return {
or: [
{ oldJson: { like: `%${value}%` } },
{ newJson: { like: `%${value}%` } },
{ description: { like: `%${value}%` } },
],
};
break;
case 'action':
if (value?.length) return { [param]: { inq: value } };
break;
function setDate(type) {
let from = dateFrom.value
? date.formatDate(dateFrom.value.split('-').reverse().join('-'), 'YYYY-MM-DD')
: undefined;
from = date.adjustDate(from, { hour: 0, minute: 0, second: 0, millisecond: 0 }, true);
let to = dateTo.value
? date.formatDate(dateTo.value.split('-').reverse().join('-'), 'YYYY-MM-DD')
: date.formatDate(dateFrom.value.split('-').reverse().join('-'), 'YYYY-MM-DD');
to = date.adjustDate(
to,
{ hour: 21, minute: 59, second: 59, millisecond: 999 },
true,
);
switch (type) {
case 'from':
return { creationDate: { gte: value } };
case 'to':
return { creationDate: { lte: value } };
case 'userType':
if (value === 'User') return { userFk: { neq: null } };
if (value === 'System') return { userFk: null };
break;
default:
return { [param]: value };
return { between: [from, to] };
case 'to': {
if (dateFrom.value) {
return {
between: [from, to],
};
}
return { lte: to };
}
}
}
function selectFilter(type, dateType) {
const filter = {};
const actions = { inq: [] };
let reload = true;
if (type === 'search') {
if (/^\s*[0-9]+\s*$/.test(searchInput.value) || props.byRecord) {
selectedFilters.value.changedModelId = searchInput.value.trim();
} else if (!searchInput.value) {
selectedFilters.value.changedModelId = undefined;
selectedFilters.value.changedModelValue = undefined;
} else {
selectedFilters.value.changedModelValue = { like: `%${searchInput.value}%` };
}
}
if (type === 'action' && selectedFilters.value.changedModel === null) {
selectedFilters.value.changedModel = undefined;
}
if (type === 'userRadio') {
selectedFilters.value.userFk = userRadio.value;
}
if (type === 'change') {
if (changeInput.value)
selectedFilters.value.or = [
{ oldJson: { like: `%${changeInput.value}%` } },
{ newJson: { like: `%${changeInput.value}%` } },
{ description: { like: `%${changeInput.value}%` } },
];
else selectedFilters.value.or = undefined;
}
if (type === 'userSelect') {
selectedFilters.value.userFk =
userSelect.value !== null ? userSelect.value : undefined;
}
if (type === 'date') {
if (!dateFrom.value && !dateTo.value) {
selectedFilters.value.creationDate = undefined;
} else if (dateType === 'to') {
selectedFilters.value.creationDate = setDate('to');
} else if (dateType === 'from') {
selectedFilters.value.creationDate = setDate('from');
}
}
Object.keys(checkboxOptions.value).forEach((key) => {
if (checkboxOptions.value[key].selected) actions.inq.push(key);
});
selectedFilters.value.action = actions.inq.length ? actions : undefined;
Object.keys(selectedFilters.value).forEach((key) => {
if (selectedFilters.value[key]) filter[key] = selectedFilters.value[key];
});
if (reload) applyFilter(filter);
}
async function clearFilter() {
selectedFilters.value = {};
byRecord.value = false;
userSelect.value = undefined;
searchInput.value = undefined;
changeInput.value = undefined;
dateFrom.value = undefined;
dateTo.value = undefined;
userRadio.value = undefined;
Object.keys(checkboxOptions.value).forEach(
(opt) => (checkboxOptions.value[opt].selected = false),
);
await applyFilter();
}
onMounted(() => {
stateStore.rightDrawerChangeValue(true);
});
onUnmounted(() => {
stateStore.rightDrawer = false;
});
@ -276,18 +383,32 @@ watch(
);
</script>
<template>
<FetchData
:url="`${props.model}Logs/${route.params.id}/models`"
:filter="{ order: ['changedModel'] }"
@on-fetch="
(data) =>
(actions = data.map((item) => {
const changedModel = item.changedModel;
return {
locale: useCapitalize(
validations[changedModel]?.locale?.name ?? changedModel,
),
value: changedModel,
};
}))
"
auto-load
/>
<VnPaginate
ref="paginate"
:data-key
:url="dataKey + 's'"
:data-key="`${model}Log`"
:url="`${model}Logs`"
:user-filter="filter"
:skeleton="false"
auto-load
@on-fetch="setLogTree"
@on-change="setLogTree"
search-url="logs"
:exprBuilder
:order="['creationDate DESC', 'id DESC']"
>
<template #body>
<div
@ -346,7 +467,6 @@ watch(
backgroundColor: useColor(modelLog.model),
}"
:title="`${modelLog.model} #${modelLog.id}`"
data-cy="vnLog-model-chip"
>
{{ t(modelLog.modelI18n) }}
</QChip>
@ -440,9 +560,10 @@ watch(
value.nameI18n
}}:
</span>
<VnLogValue
:value="value.val"
:name="value.name"
<VnJsonValue
:value="
value.val.val
"
/>
</QItem>
</QCardSection>
@ -460,7 +581,6 @@ watch(
}`,
)
"
data-cy="vnLog-action-icon"
/>
</div>
</QItem>
@ -494,10 +614,7 @@ watch(
>
{{ prop.nameI18n }}:
</span>
<VnLogValue
:value="prop.val"
:name="prop.name"
/>
<VnJsonValue :value="prop.val.val" />
<span
v-if="
propIndex <
@ -524,10 +641,17 @@ watch(
>
{{ prop.nameI18n }}:
</span>
<VnJsonValue :value="prop.val.val" />
<span
v-if="prop.val.id"
class="id-value"
>
#{{ prop.val.id }}
</span>
<span v-if="log.action == 'update'">
<VnLogValue
:value="prop.old"
:name="prop.name"
<VnJsonValue
:value="prop.old.val"
/>
<span
v-if="prop.old.id"
@ -535,28 +659,6 @@ watch(
>
#{{ prop.old.id }}
</span>
<VnLogValue
:value="prop.val"
:name="prop.name"
/>
<span
v-if="prop.val.id"
class="id-value"
>
#{{ prop.val.id }}
</span>
</span>
<span v-else="prop.old.val">
<VnLogValue
:value="prop.val"
:name="prop.name"
/>
<span
v-if="prop.old.id"
class="id-value"
>#{{ prop.old.id }}</span
>
</span>
</div>
</span>
@ -578,12 +680,176 @@ watch(
</VnPaginate>
<RightMenu>
<template #right-panel>
<VnLogFilter :data-key />
<QList dense>
<QSeparator />
<QItem class="q-mt-sm">
<QInput
:label="t('globals.search')"
v-model="searchInput"
class="full-width"
clearable
clear-icon="close"
@keyup.enter="() => selectFilter('search')"
@focusout="() => selectFilter('search')"
@clear="() => selectFilter('search')"
>
<template #append>
<QIcon name="info" class="cursor-pointer">
<QTooltip>{{ t('tooltips.search') }}</QTooltip>
</QIcon>
</template>
</QInput>
</QItem>
<QItem>
<VnSelect
class="full-width"
:label="t('globals.entity')"
v-model="selectedFilters.changedModel"
option-label="locale"
option-value="value"
:options="actions"
@update:model-value="selectFilter('action')"
hide-selected
/>
</QItem>
<QItem class="q-mt-sm">
<QOptionGroup
size="sm"
v-model="userRadio"
:options="userTypes"
color="primary"
@update:model-value="selectFilter('userRadio')"
right-label
>
<template #label="{ label }">
{{ t(`Users.${label}`) }}
</template>
</QOptionGroup>
</QItem>
<QItem class="q-mt-sm">
<QItemSection v-if="userRadio !== null">
<VnSelect
class="full-width"
:label="t('globals.user')"
v-model="userSelect"
option-label="name"
option-value="id"
:url="`${model}Logs/${route.params.id}/editors`"
:fields="['id', 'nickname', 'name', 'image']"
sort-by="nickname"
@update:model-value="selectFilter('userSelect')"
hide-selected
>
<template #option="{ opt, itemProps }">
<QItem
v-bind="itemProps"
class="q-pa-xs row items-center"
>
<QItemSection class="col-3 items-center">
<VnAvatar :worker-id="opt.id" />
</QItemSection>
<QItemSection class="col-9 justify-center">
<span>{{ opt.name }}</span>
<span class="text-grey">{{ opt.nickname }}</span>
</QItemSection>
</QItem>
</template>
</VnSelect>
</QItemSection>
</QItem>
<QItem class="q-mt-sm">
<QInput
:label="t('globals.changes')"
v-model="changeInput"
class="full-width"
clearable
clear-icon="close"
@keyup.enter="selectFilter('change')"
@focusout="selectFilter('change')"
@clear="selectFilter('change')"
>
<template #append>
<QIcon name="info" class="cursor-pointer">
<QTooltip max-width="250px">{{
t('tooltips.changes')
}}</QTooltip>
</QIcon>
</template>
</QInput>
</QItem>
<QItem
:class="index == 'create' ? 'q-mt-md' : 'q-mt-xs'"
v-for="(checkboxOption, index) in checkboxOptions"
:key="index"
>
<QCheckbox
size="sm"
v-model="checkboxOption.selected"
:label="t(`actions.${checkboxOption.label}`)"
@update:model-value="selectFilter"
/>
</QItem>
<QItem class="q-mt-sm">
<QInput
class="full-width"
:label="t('globals.date')"
@click="dateFromDialog = true"
@focus="(evt) => evt.target.blur()"
@clear="selectFilter('date', 'to')"
v-model="dateFrom"
clearable
clear-icon="close"
/>
</QItem>
<QItem class="q-mt-sm">
<QInput
class="full-width"
:label="t('globals.to')"
@click="dateToDialog = true"
@focus="(evt) => evt.target.blur()"
@clear="selectFilter('date', 'from')"
v-model="dateTo"
clearable
clear-icon="close"
/>
</QItem>
</QList>
</template>
</RightMenu>
<QDialog v-model="dateFromDialog">
<QDate
:years-in-month-view="false"
v-model="dateFrom"
dense
flat
minimal
@update:model-value="
(value) => {
dateFromDialog = false;
dateFrom = date.formatDate(value, 'DD-MM-YYYY');
selectFilter('date', 'from');
}
"
/>
</QDialog>
<QDialog v-model="dateToDialog">
<QDate
v-model="dateTo"
dense
flat
minimal
@update:model-value="
(value) => {
dateToDialog = false;
dateTo = date.formatDate(value, 'DD-MM-YYYY');
selectFilter('date', 'to');
}
"
/>
</QDialog>
<QPageSticky position="bottom-right" :offset="[25, 25]">
<QBtn
v-if="Object.keys(userParams).some((filter) => filter !== 'originFk')"
v-if="Object.values(selectedFilters).some((filter) => filter !== undefined)"
color="primary"
icon="filter_alt_off"
size="md"

View File

@ -1,249 +1,77 @@
<script setup>
import { ref } from 'vue';
import { useI18n } from 'vue-i18n';
import VnTableFilter from '../VnTable/VnTableFilter.vue';
import VnSelect from './VnSelect.vue';
import { useRoute } from 'vue-router';
import VnInput from './VnInput.vue';
import { ref, computed, watch } from 'vue';
import VnInputDate from './VnInputDate.vue';
import { useFilterParams } from 'src/composables/useFilterParams';
import FetchData from '../FetchData.vue';
import { useValidator } from 'src/composables/useValidator';
import { useCapitalize } from 'src/composables/useCapitalize';
import FetchData from 'components/FetchData.vue';
import VnFilterPanel from 'src/components/ui/VnFilterPanel.vue';
const $props = defineProps({
const { t } = useI18n();
const props = defineProps({
dataKey: {
type: String,
default: null,
required: true,
},
});
const { t } = useI18n();
const route = useRoute();
const validationsStore = useValidator();
const { models } = validationsStore;
const entities = ref([]);
const editors = ref([]);
const userParams = ref(useFilterParams($props.dataKey).params);
let validations = models;
const userTypes = [
{ value: 'All', label: t(`Users.All`) },
{ value: 'User', label: t(`Users.User`) },
{ value: 'System', label: t(`Users.System`) },
];
const checkboxOptions = ref([
{ name: 'insert', label: 'Creates', selected: false },
{ name: 'update', label: 'Edits', selected: false },
{ name: 'delete', label: 'Deletes', selected: false },
{ name: 'select', label: 'Accesses', selected: false },
]);
const columns = computed(() => [
{ name: 'changedModelValue' },
{ name: 'changedModel' },
{ name: 'userType', orderBy: false },
{ name: 'userFk' },
{ name: 'change', orderBy: false },
{ name: 'action' },
{ name: 'from', orderBy: 'creationDate' },
{ name: 'to', orderBy: 'creationDate' },
]);
const userParamsWatcher = watch(
() => userParams.value,
(params) => {
if (params.action) {
params.action.forEach((option) => {
checkboxOptions.value.find((o) => o.name === option).selected = true;
});
userParamsWatcher();
}
},
);
function getActions() {
const actions = checkboxOptions.value
.filter((option) => option.selected)
?.map((o) => o.name);
return actions.length ? actions : null;
}
const workers = ref();
</script>
<template>
<FetchData
:url="`${dataKey}s/${route.params.id}/models`"
:filter="{ order: ['changedModel'] }"
@on-fetch="
(data) =>
(entities = data.map((item) => {
const changedModel = item.changedModel;
return {
locale: useCapitalize(
validations[changedModel]?.locale?.name ?? changedModel,
),
value: changedModel,
};
}))
"
url="Workers/activeWithInheritedRole"
:filter="{ where: { role: 'salesPerson' } }"
@on-fetch="(data) => (workers = data)"
auto-load
/>
<FetchData
:url="`${dataKey}s/${route.params.id}/editors`"
:filter="{ fields: ['id', 'nickname', 'name', 'image'] }"
sort-by="nickname"
@on-fetch="(data) => (editors = data)"
auto-load
/>
<VnTableFilter
v-if="dataKey"
:data-key
:columns="columns"
:redirect="false"
:hiddenTags="['originFk', 'creationDate']"
:exprBuilder
search-url="logs"
:showTagChips="false"
>
<template #filter-changedModelValue="{ params, columnName, searchFn }">
<VnInput
:label="t('globals.search')"
v-model="params[columnName]"
@keyup.enter="searchFn"
@blur="searchFn"
@remove="searchFn"
:info="t('tooltips.search')"
dense
filled
data-cy="vnLog-search"
/>
</template>
<template #filter-changedModel="{ params, columnName, searchFn }">
<VnSelect
:label="t('globals.entity')"
v-model="params[columnName]"
option-label="locale"
option-value="value"
:options="entities"
@update:model-value="() => searchFn()"
dense
filled
data-cy="vnLog-entity"
/>
</template>
<template #filter-userType="{ params, columnName, searchFn }">
<QOptionGroup
class="text-left"
size="sm"
v-model="params[columnName]"
:options="userTypes"
color="primary"
@update:model-value="
() => {
params.userFk = null;
searchFn();
}
"
/>
</template>
<template #filter-userFk="{ params, columnName, searchFn }">
<VnSelect
:label="t('globals.user')"
v-model="params[columnName]"
:options="editors"
@update:modelValue="() => searchFn()"
:disable="params.userType === 'System'"
dense
filled
>
<template #option="{ opt, itemProps }">
<QItem v-bind="itemProps" class="q-pa-xs row items-center">
<QItemSection class="col-3 items-center">
<VnAvatar :worker-id="opt.id" />
</QItemSection>
<QItemSection class="col-9 justify-center">
<span>{{ opt.name }}</span>
<span class="text-grey">{{ opt.nickname }}</span>
</QItemSection>
</QItem>
</template>
</VnSelect>
</template>
<template #filter-change="{ params, columnName, searchFn }">
<VnInput
:label="t('globals.changes')"
v-model="params[columnName]"
@keyup.enter="searchFn"
@blur="searchFn"
@remove="searchFn"
:info="t('tooltips.changes')"
dense
filled
/>
</template>
<template #filter-action="{ searchFn }">
<div class="column">
<QCheckbox
v-for="checkboxOption in checkboxOptions"
:key="checkboxOption"
size="sm"
v-model="checkboxOption.selected"
:label="t(`actions.${checkboxOption.label}`)"
@update:model-value="
() => searchFn(undefined, 'action', getActions())
"
data-cy="vnLog-checkbox"
/>
<VnFilterPanel :data-key="props.dataKey" :search-button="true">
<template #tags="{ tag, formatFn }">
<div class="q-gutter-x-xs">
<strong>{{ t(`params.${tag.label}`) }}: </strong>
<span>{{ formatFn(tag.value) }}</span>
</div>
</template>
<template #filter-from="{ params, columnName, searchFn }">
<VnInputDate
:label="t('globals.from')"
v-model="params[columnName]"
<template #body="{ params, searchFn }">
<QDate
v-model="params.created"
@update:model-value="searchFn()"
dense
filled
@update:modelValue="() => searchFn()"
/>
flat
minimal
>
</QDate>
<QSeparator />
<QItem>
<QItemSection v-if="!workers">
<QSkeleton type="QInput" class="full-width" />
</QItemSection>
<QItemSection v-if="workers">
<QSelect
:label="t('User')"
v-model="params.userFk"
@update:model-value="searchFn()"
:options="workers"
option-value="id"
option-label="name"
emit-value
map-options
use-input
:input-debounce="0"
/>
</QItemSection>
</QItem>
</template>
<template #filter-to="{ params, columnName, searchFn }">
<VnInputDate
:label="t('globals.to')"
v-model="params[columnName]"
dense
filled
@update:modelValue="() => searchFn()"
/>
</template>
</VnTableFilter>
</VnFilterPanel>
</template>
<i18n>
es:
tooltips:
search: Buscar por identificador o concepto
changes: Buscar por cambios. Los atributos deben buscarse por su nombre interno, para obtenerlo situar el cursor sobre el atributo.
actions:
Creates: Crea
Edits: Modifica
Deletes: Elimina
Accesses: Accede
Users:
User: Usuario
All: Todo
System: Sistema
params:
changedModel: Entity
<i18n>
en:
tooltips:
search: Search by identifier or concept
changes: Search by changes. Attributes must be searched by their internal name, to get it place the cursor over the attribute.
actions:
Creates: Creates
Edits: Edits
Deletes: Deletes
Accesses: Accesses
Users:
User: User
All: All
System: System
params:
changedModel: Entidad
search: Contains
userFk: User
created: Created
es:
params:
search: Contiene
userFk: Usuario
created: Creada
User: Usuario
</i18n>

View File

@ -1,28 +0,0 @@
<script setup>
import { useDescriptorStore } from 'src/stores/useDescriptorStore';
import VnJsonValue from './VnJsonValue.vue';
import { computed } from 'vue';
const descriptorStore = useDescriptorStore();
const $props = defineProps({
value: { type: Object, default: () => {} },
name: { type: String, default: undefined },
});
const descriptor = computed(() => descriptorStore.has($props.name));
</script>
<template>
<VnJsonValue :value="value.val" />
<span
v-if="(value.id || typeof value.val == 'number') && descriptor"
style="margin-left: 2px"
>
<QIcon
name="launch"
class="link"
:data-cy="'iconLaunch-' + $props.name"
style="padding-bottom: 2px"
/>
<component :is="descriptor" :id="value.id ?? value.val" />
</span>
</template>

View File

@ -12,7 +12,7 @@ const $props = defineProps({
},
});
onMounted(
() => (stateStore.leftDrawer = useQuasar().screen.gt.xs ? $props.leftDrawer : false),
() => (stateStore.leftDrawer = useQuasar().screen.gt.xs ? $props.leftDrawer : false)
);
const teleportRef = ref({});
@ -35,14 +35,8 @@ onMounted(() => {
<template>
<QDrawer v-model="stateStore.leftDrawer" show-if-above :width="256">
<QScrollArea class="fit text-grey-8">
<div id="left-panel" ref="teleportRef">
<template v-if="stateStore.cardDescriptor">
<component :is="stateStore.cardDescriptor" />
<QSeparator />
<LeftMenu source="card" />
</template>
<template v-else> <LeftMenu /></template>
</div>
<div id="left-panel" ref="teleportRef"></div>
<LeftMenu v-if="!hasContent" />
</QScrollArea>
</QDrawer>
<QPageContainer>

View File

@ -1,38 +0,0 @@
<script setup>
import { ref } from 'vue';
defineProps({
label: {
type: String,
default: '',
},
icon: {
type: String,
required: true,
default: null,
},
color: {
type: String,
default: 'primary',
},
tooltip: {
type: String,
default: null,
},
});
const popupProxyRef = ref(null);
</script>
<template>
<QBtn :color="$props.color" :icon="$props.icon" :label="$t($props.label)">
<template #default>
<slot name="extraIcon"></slot>
<QPopupProxy ref="popupProxyRef" style="max-width: none">
<QCard>
<slot :popup="popupProxyRef"></slot>
</QCard>
</QPopupProxy>
<QTooltip>{{ $t($props.tooltip) }}</QTooltip>
</template>
</QBtn>
</template>

View File

@ -40,6 +40,10 @@ const $props = defineProps({
type: Boolean,
default: true,
},
keepData: {
type: Boolean,
default: true,
},
});
const route = useRoute();
@ -57,6 +61,7 @@ onBeforeMount(() => {
if ($props.dataKey)
arrayData = useArrayData($props.dataKey, {
searchUrl: 'table',
keepData: $props.keepData,
...$props.arrayDataProps,
navigate: $props.redirect,
});

View File

@ -1,5 +1,5 @@
<script setup>
import { ref, toRefs, computed, watch, onMounted, useAttrs, nextTick } from 'vue';
import { ref, toRefs, computed, watch, onMounted, useAttrs } from 'vue';
import { useI18n } from 'vue-i18n';
import { useArrayData } from 'src/composables/useArrayData';
import { useRequired } from 'src/composables/useRequired';
@ -152,10 +152,6 @@ const value = computed({
},
});
const computedSortBy = computed(() => {
return $props.sortBy || $props.optionLabel + ' ASC';
});
watch(options, (newValue) => {
setOptions(newValue);
});
@ -175,8 +171,7 @@ onMounted(() => {
});
const arrayDataKey =
$props.dataKey ??
($props.url?.length > 0 ? $props.url : ($attrs.name ?? $attrs.label));
$props.dataKey ?? ($props.url?.length > 0 ? $props.url : $attrs.name ?? $attrs.label);
const arrayData = useArrayData(arrayDataKey, {
url: $props.url,
@ -190,7 +185,7 @@ function findKeyInOptions() {
}
function setOptions(data) {
data = dataByOrder(data, computedSortBy.value);
data = dataByOrder(data, $props.sortBy);
myOptions.value = JSON.parse(JSON.stringify(data));
myOptionsOriginal.value = JSON.parse(JSON.stringify(data));
emit('update:options', data);
@ -220,13 +215,12 @@ function filter(val, options) {
async function fetchFilter(val) {
if (!$props.url) return;
const { fields, include, limit } = $props;
const sortBy = computedSortBy.value;
const { fields, include, sortBy, limit } = $props;
const key =
optionFilterValue.value ??
(new RegExp(/\d/g).test(val)
? optionValue.value
: (optionFilter.value ?? optionLabel.value));
: optionFilter.value ?? optionLabel.value);
let defaultWhere = {};
if ($props.filterOptions.length) {
@ -245,14 +239,13 @@ async function fetchFilter(val) {
const { data } = await arrayData.applyFilter(
{ filter: filterOptions },
{ updateRouter: false },
{ updateRouter: false }
);
setOptions(data);
return data;
}
async function filterHandler(val, update) {
if (isLoading.value) return update();
if (!val && lastVal.value === val) {
lastVal.value = val;
return update();
@ -279,7 +272,7 @@ async function filterHandler(val, update) {
ref.setOptionIndex(-1);
ref.moveOptionSelection(1, true);
}
},
}
);
}
@ -300,7 +293,6 @@ async function onScroll({ to, direction, from, index }) {
await arrayData.loadMore();
setOptions(arrayData.store.data);
vnSelectRef.value.scrollTo(lastIndex);
await nextTick();
isLoading.value = false;
}
}
@ -316,7 +308,7 @@ function handleKeyDown(event) {
if (inputValue) {
const matchingOption = myOptions.value.find(
(option) =>
option[optionLabel.value].toLowerCase() === inputValue.toLowerCase(),
option[optionLabel.value].toLowerCase() === inputValue.toLowerCase()
);
if (matchingOption) {
@ -328,11 +320,11 @@ function handleKeyDown(event) {
}
const focusableElements = document.querySelectorAll(
'a:not([disabled]), button:not([disabled]), input:not([disabled]), textarea:not([disabled]), select:not([disabled]), details:not([disabled]), [tabindex]:not([tabindex="-1"]):not([disabled])',
'a:not([disabled]), button:not([disabled]), input:not([disabled]), textarea:not([disabled]), select:not([disabled]), details:not([disabled]), [tabindex]:not([tabindex="-1"]):not([disabled])'
);
const currentIndex = Array.prototype.indexOf.call(
focusableElements,
event.target,
event.target
);
if (currentIndex >= 0 && currentIndex < focusableElements.length - 1) {
focusableElements[currentIndex + 1].focus();

View File

@ -14,7 +14,7 @@ const $props = defineProps({
},
});
const options = ref([]);
const emit = defineEmits(['blur']);
onBeforeMount(async () => {
const { url, optionValue, optionLabel } = useAttrs();
const findBy = $props.find ?? url?.charAt(0)?.toLocaleLowerCase() + url?.slice(1, -1);
@ -35,5 +35,5 @@ onBeforeMount(async () => {
});
</script>
<template>
<VnSelect v-bind="$attrs" :options="$attrs.options ?? options" @blur="emit('blur')" />
<VnSelect v-bind="$attrs" :options="$attrs.options ?? options" />
</template>

View File

@ -37,6 +37,7 @@ const isAllowedToCreate = computed(() => {
defineExpose({ vnSelectDialogRef: select });
</script>
<template>
<VnSelect
ref="select"
@ -66,6 +67,7 @@ defineExpose({ vnSelectDialogRef: select });
</template>
</VnSelect>
</template>
<style lang="scss" scoped>
.default-icon {
cursor: pointer;

View File

@ -1,7 +1,9 @@
<script setup>
import { computed } from 'vue';
import VnSelect from 'components/common/VnSelect.vue';
const model = defineModel({ type: [String, Number, Object] });
const url = 'Suppliers';
</script>
<template>
@ -9,13 +11,11 @@ const model = defineModel({ type: [String, Number, Object] });
:label="$t('globals.supplier')"
v-bind="$attrs"
v-model="model"
url="Suppliers"
:url="url"
option-value="id"
option-label="nickname"
:fields="['id', 'name', 'nickname', 'nif']"
:filter-options="['id', 'name', 'nickname', 'nif']"
sort-by="name ASC"
data-cy="vnSupplierSelect"
>
<template #option="scope">
<QItem v-bind="scope.itemProps">

View File

@ -1,50 +0,0 @@
<script setup>
import VnSelectDialog from './VnSelectDialog.vue';
import FilterTravelForm from 'src/components/FilterTravelForm.vue';
import { useI18n } from 'vue-i18n';
import { toDate } from 'src/filters';
const { t } = useI18n();
const $props = defineProps({
data: {
type: Object,
required: true,
},
onFilterTravelSelected: {
type: Function,
required: true,
},
});
</script>
<template>
<VnSelectDialog
:label="t('entry.basicData.travel')"
v-bind="$attrs"
url="Travels/filter"
:fields="['id', 'warehouseInName']"
option-value="id"
option-label="warehouseInName"
map-options
hide-selected
:required="true"
action-icon="filter_alt"
:roles-allowed-to-create="['buyer']"
>
<template #form>
<FilterTravelForm @travel-selected="onFilterTravelSelected(data, $event)" />
</template>
<template #option="scope">
<QItem v-bind="scope.itemProps">
<QItemSection>
<QItemLabel>
{{ scope.opt?.agencyModeName }} -
{{ scope.opt?.warehouseInName }}
({{ toDate(scope.opt?.shipped) }})
{{ scope.opt?.warehouseOutName }}
({{ toDate(scope.opt?.landed) }})
</QItemLabel>
</QItemSection>
</QItem>
</template>
</VnSelectDialog>
</template>

View File

@ -4,15 +4,12 @@ import { vi, afterEach, beforeAll, describe, expect, it } from 'vitest';
describe('VnDmsList', () => {
let vm;
const dms = {
userFk: 1,
name: 'DMS 1',
const dms = {
userFk: 1,
name: 'DMS 1'
};
beforeAll(() => {
vi.mock('src/composables/getUrl', () => ({
getUrl: vi.fn().mockResolvedValue(''),
}));
vi.spyOn(axios, 'get').mockResolvedValue({ data: [] });
vm = createWrapper(VnDmsList, {
props: {
@ -21,8 +18,8 @@ describe('VnDmsList', () => {
filter: 'wd.workerFk',
updateModel: 'Workers',
deleteModel: 'WorkerDms',
downloadModel: 'WorkerDms',
},
downloadModel: 'WorkerDms'
}
}).vm;
});
@ -32,45 +29,46 @@ describe('VnDmsList', () => {
describe('setData()', () => {
const data = [
{
userFk: 1,
{
userFk: 1,
name: 'Jessica',
lastName: 'Jones',
file: '4.jpg',
created: '2021-07-28 21:00:00',
created: '2021-07-28 21:00:00'
},
{
userFk: 2,
{
userFk: 2,
name: 'Bruce',
lastName: 'Banner',
created: '2022-07-28 21:00:00',
dms: {
userFk: 2,
userFk: 2,
name: 'Bruce',
lastName: 'BannerDMS',
created: '2022-07-28 21:00:00',
file: '4.jpg',
},
}
},
{
userFk: 3,
name: 'Natasha',
lastName: 'Romanoff',
file: '4.jpg',
created: '2021-10-28 21:00:00',
},
];
created: '2021-10-28 21:00:00'
}
]
it('Should replace objects that contain the "dms" property with the value of the same and sort by creation date', () => {
vm.setData(data);
expect([vm.rows][0][0].lastName).toEqual('BannerDMS');
expect([vm.rows][0][1].lastName).toEqual('Romanoff');
});
});
describe('parseDms()', () => {
const resultDms = { ...dms, userId: 1 };
const resultDms = { ...dms, userId:1};
it('Should add properties that end with "Fk" by changing the suffix to "Id"', () => {
const parsedDms = vm.parseDms(dms);
expect(parsedDms).toEqual(resultDms);
@ -78,12 +76,12 @@ describe('VnDmsList', () => {
});
describe('showFormDialog()', () => {
const resultDms = { ...dms, userId: 1 };
const resultDms = { ...dms, userId:1};
it('should call fn parseDms() and set show true if dms is defined', () => {
vm.showFormDialog(dms);
expect(vm.formDialog.show).toEqual(true);
expect(vm.formDialog.dms).toEqual(resultDms);
});
});
});
});

View File

@ -108,4 +108,27 @@ describe('VnLog', () => {
expect(vm.logTree[0].originFk).toEqual(1);
expect(vm.logTree[0].logs[0].user.name).toEqual('salesPerson');
});
it('should correctly set the selectedFilters when filtering', () => {
vm.searchInput = '1';
vm.userSelect = '21';
vm.checkboxOptions.insert.selected = true;
vm.checkboxOptions.update.selected = true;
vm.selectFilter('search');
vm.selectFilter('userSelect');
expect(vm.selectedFilters.changedModelId).toEqual('1');
expect(vm.selectedFilters.userFk).toEqual('21');
expect(vm.selectedFilters.action).toEqual({ inq: ['insert', 'update'] });
});
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

@ -1,28 +0,0 @@
import { vi, describe, expect, it, beforeAll, afterEach } from 'vitest';
import { createWrapper } from 'app/test/vitest/helper';
import VnLogFilter from 'src/components/common/VnLogFilter.vue';
describe('VnLogFilter', () => {
let vm;
beforeAll(async () => {
vm = createWrapper(VnLogFilter, {
props: {
dataKey: 'ClaimLog',
},
}).vm;
});
afterEach(() => {
vi.clearAllMocks();
});
it('should getActions selected', async () => {
vm.checkboxOptions.find((o) => o.name == 'insert').selected = true;
vm.checkboxOptions.find((o) => o.name == 'update').selected = true;
const actions = vm.getActions();
expect(actions.length).toEqual(2);
expect(actions).toEqual(['insert', 'update']);
});
});

View File

@ -1,26 +0,0 @@
import { describe, it, expect } from 'vitest';
import VnLogValue from 'src/components/common/VnLogValue.vue';
import { createWrapper } from 'app/test/vitest/helper';
const buildComponent = (props) => {
return createWrapper(VnLogValue, {
props,
global: {},
}).wrapper;
};
describe('VnLogValue', () => {
const id = 1;
it('renders without descriptor', async () => {
expect(getIcon('inventFk').exists()).toBe(false);
});
it('renders with descriptor', async () => {
expect(getIcon('claimFk').text()).toBe('launch');
});
function getIcon(name) {
const wrapper = buildComponent({ value: { val: id }, name });
return wrapper.find('.q-icon');
}
});

View File

@ -1,6 +1,16 @@
import { describe, it, expect, vi, afterEach, beforeEach, afterAll } from 'vitest';
import {
describe,
it,
expect,
vi,
beforeAll,
afterEach,
beforeEach,
afterAll,
} from 'vitest';
import { createWrapper, axios } from 'app/test/vitest/helper';
import VnNotes from 'src/components/ui/VnNotes.vue';
import vnDate from 'src/boot/vnDate';
describe('VnNotes', () => {
let vm;
@ -8,7 +18,6 @@ describe('VnNotes', () => {
let spyFetch;
let postMock;
let patchMock;
let deleteMock;
let expectedInsertBody;
let expectedUpdateBody;
const defaultOptions = {
@ -48,7 +57,6 @@ describe('VnNotes', () => {
beforeEach(() => {
postMock = vi.spyOn(axios, 'post');
patchMock = vi.spyOn(axios, 'patch');
deleteMock = vi.spyOn(axios, 'delete');
});
afterEach(() => {
@ -145,16 +153,4 @@ describe('VnNotes', () => {
);
});
});
describe('delete', () => {
it('Should call axios.delete with url and vnPaginateRef.fetch', async () => {
generateWrapper();
createSpyFetch();
await vm.deleteNote({ id: 1 });
expect(deleteMock).toHaveBeenCalledWith(`${vm.$props.url}/1`);
expect(spyFetch).toHaveBeenCalled();
});
});
});

View File

@ -1,38 +1,296 @@
<script setup>
import { ref } from 'vue';
import VnDescriptor from './VnDescriptor.vue';
import { onBeforeMount, watch, computed, ref } from 'vue';
import { useI18n } from 'vue-i18n';
import SkeletonDescriptor from 'components/ui/SkeletonDescriptor.vue';
import { useArrayData } from 'composables/useArrayData';
import { useSummaryDialog } from 'src/composables/useSummaryDialog';
import { useState } from 'src/composables/useState';
import { useRoute } from 'vue-router';
import VnMoreOptions from './VnMoreOptions.vue';
const $props = defineProps({
id: {
type: Number,
default: false,
url: {
type: String,
default: '',
},
card: {
filter: {
type: Object,
default: null,
},
title: {
type: String,
default: '',
},
subtitle: {
type: Number,
default: null,
},
dataKey: {
type: String,
default: null,
},
module: {
type: String,
default: null,
},
summary: {
type: Object,
default: null,
},
width: {
type: String,
default: 'md-width',
},
});
const state = useState();
const route = useRoute();
const { t } = useI18n();
const { viewSummary } = useSummaryDialog();
let arrayData;
let store;
let entity;
const isLoading = ref(false);
const isSameDataKey = computed(() => $props.dataKey === route.meta.moduleName);
defineExpose({ getData });
onBeforeMount(async () => {
arrayData = useArrayData($props.dataKey, {
url: $props.url,
filter: $props.filter,
skip: 0,
oneRecord: true,
});
store = arrayData.store;
entity = computed(() => {
const data = store.data ?? {};
if (data) emit('onFetch', data);
return data;
});
// It enables to load data only once if the module is the same as the dataKey
if (!isSameDataKey.value || !route.params.id) await getData();
watch(
() => [$props.url, $props.filter],
async () => {
if (!isSameDataKey.value) await getData();
},
);
});
async function getData() {
store.url = $props.url;
store.filter = $props.filter ?? {};
isLoading.value = true;
try {
const { data } = await arrayData.fetch({ append: false, updateRouter: false });
state.set($props.dataKey, data);
emit('onFetch', data);
} finally {
isLoading.value = false;
}
}
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;
}
const emit = defineEmits(['onFetch']);
const entity = ref();
const iconModule = computed(() => route.matched[1].meta.icon);
const toModule = computed(() =>
route.matched[1].path.split('/').length > 2
? route.matched[1].redirect
: route.matched[1].children[0].redirect,
);
</script>
<template>
<component
:is="card"
:id
:visual="false"
v-bind="$attrs"
@on-fetch="
(data) => {
entity = data;
emit('onFetch', data);
}
"
/>
<VnDescriptor v-model="entity" v-bind="$attrs">
<template v-for="(_, slotName) in $slots" #[slotName]="slotData" :key="slotName">
<slot :name="slotName" v-bind="slotData ?? {}" :key="slotName" />
<div class="descriptor">
<template v-if="entity && !isLoading">
<div class="header bg-primary q-pa-sm justify-between">
<slot name="header-extra-action"
><QBtn
round
flat
dense
size="md"
:icon="iconModule"
color="white"
class="link"
:to="$attrs['to-module'] ?? toModule"
>
<QTooltip>
{{ t('globals.goToModuleIndex') }}
</QTooltip>
</QBtn></slot
>
<QBtn
@click.stop="viewSummary(entity.id, $props.summary, $props.width)"
round
flat
dense
size="md"
icon="preview"
color="white"
class="link"
v-if="summary"
>
<QTooltip>
{{ t('components.smartCard.openSummary') }}
</QTooltip>
</QBtn>
<RouterLink :to="{ name: `${module}Summary`, params: { id: entity.id } }">
<QBtn
class="link"
color="white"
dense
flat
icon="launch"
round
size="md"
>
<QTooltip>
{{ t('components.cardDescriptor.summary') }}
</QTooltip>
</QBtn>
</RouterLink>
<VnMoreOptions v-if="$slots.menu">
<template #menu="{ menuRef }">
<slot name="menu" :entity="entity" :menu-ref="menuRef" />
</template>
</VnMoreOptions>
</div>
<slot name="before" />
<div class="body q-py-sm">
<QList dense>
<QItemLabel header class="ellipsis text-h5" :lines="1">
<div class="title">
<span v-if="$props.title" :title="getValueFromPath(title)">
{{ getValueFromPath(title) ?? $props.title }}
</span>
<slot v-else name="description" :entity="entity">
<span :title="entity.name">
{{ entity.name }}
</span>
</slot>
</div>
</QItemLabel>
<QItem dense>
<QItemLabel class="subtitle" caption>
#{{ getValueFromPath(subtitle) ?? entity.id }}
</QItemLabel>
</QItem>
</QList>
<div class="list-box q-mt-xs">
<slot name="body" :entity="entity" />
</div>
</div>
<div class="icons">
<slot name="icons" :entity="entity" />
</div>
<div class="actions justify-center">
<slot name="actions" :entity="entity" />
</div>
<slot name="after" />
</template>
</VnDescriptor>
<!-- Skeleton -->
<SkeletonDescriptor v-if="!entity || isLoading" />
</div>
<QInnerLoading
:label="t('globals.pleaseWait')"
:showing="isLoading"
color="primary"
/>
</template>
<style lang="scss">
.body {
background-color: var(--vn-section-color);
.text-h5 {
font-size: 20px;
padding-top: 5px;
padding-bottom: 0px;
}
.q-item {
min-height: 20px;
.link {
margin-left: 10px;
}
}
.vn-label-value {
display: flex;
padding: 0px 16px;
.label {
color: var(--vn-label-color);
font-size: 14px;
&:not(:has(a))::after {
content: ':';
}
}
.value {
color: var(--vn-text-color);
font-size: 14px;
margin-left: 4px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
text-align: left;
}
.info {
margin-left: 5px;
}
}
}
</style>
<style lang="scss" scoped>
.title {
overflow: hidden;
text-overflow: ellipsis;
span {
color: var(--vn-text-color);
font-weight: bold;
}
}
.subtitle {
color: var(--vn-text-color);
font-size: 16px;
margin-bottom: 2px;
}
.list-box {
.q-item__label {
color: var(--vn-label-color);
padding-bottom: 0%;
}
}
.descriptor {
width: 256px;
.header {
display: flex;
align-items: center;
}
.icons {
margin: 0 10px;
display: flex;
justify-content: center;
.q-icon {
margin-right: 5px;
}
}
.actions {
margin: 0 5px;
justify-content: center !important;
}
}
</style>

View File

@ -81,7 +81,6 @@ async function fetch() {
name: `${moduleName ?? route.meta.moduleName}Summary`,
params: { id: entityId || entity.id },
}"
data-cy="goToSummaryBtn"
>
<QIcon name="open_in_new" color="white" size="sm" />
</router-link>
@ -159,7 +158,6 @@ async function fetch() {
display: flex;
flex-direction: row;
margin-top: 2px;
align-items: start;
.label {
color: var(--vn-label-color);
width: 9em;
@ -170,10 +168,6 @@ async function fetch() {
flex-grow: 0;
flex-shrink: 0;
}
&.ellipsis > .value {
text-overflow: ellipsis;
white-space: pre;
}
.value {
color: var(--vn-text-color);
overflow: hidden;
@ -206,29 +200,6 @@ async function fetch() {
}
}
}
.vn-card-group {
display: flex;
flex-direction: column;
}
.vn-card-content {
display: flex;
flex-direction: column;
text-overflow: ellipsis;
> div {
max-height: 70px;
}
}
@media (min-width: 1010px) {
.vn-card-group {
flex-direction: row;
}
.vn-card-content {
flex: 1;
}
}
</style>
<style lang="scss" scoped>
.summaryHeader .vn-label-value {

View File

@ -132,8 +132,7 @@ const card = toRef(props, 'item');
display: flex;
flex-direction: column;
gap: 4px;
white-space: nowrap;
width: 192px;
p {
margin-bottom: 0;
}

View File

@ -1,78 +0,0 @@
<script setup>
import { onBeforeMount, watch, computed, ref } from 'vue';
import { useArrayData } from 'composables/useArrayData';
import { useState } from 'src/composables/useState';
import { useRoute } from 'vue-router';
import VnDescriptor from './VnDescriptor.vue';
const $props = defineProps({
url: {
type: String,
default: '',
},
filter: {
type: Object,
default: null,
},
dataKey: {
type: String,
default: null,
},
});
const state = useState();
const route = useRoute();
let arrayData;
let store;
let entity;
const isLoading = ref(false);
const isSameDataKey = computed(() => $props.dataKey === route.meta.moduleName);
defineExpose({ getData });
onBeforeMount(async () => {
arrayData = useArrayData($props.dataKey, {
url: $props.url,
userFilter: $props.filter,
skip: 0,
oneRecord: true,
});
store = arrayData.store;
entity = computed(() => {
const data = store.data ?? {};
if (data) emit('onFetch', data);
return data;
});
// It enables to load data only once if the module is the same as the dataKey
if (!isSameDataKey.value || !route.params.id) await getData();
watch(
() => [$props.url, $props.filter],
async () => {
if (!isSameDataKey.value) await getData();
},
);
});
async function getData() {
store.url = $props.url;
store.filter = $props.filter ?? {};
isLoading.value = true;
try {
const { data } = await arrayData.fetch({ append: false, updateRouter: false });
state.set($props.dataKey, data);
emit('onFetch', data);
} finally {
isLoading.value = false;
}
}
const emit = defineEmits(['onFetch']);
</script>
<template>
<VnDescriptor v-model="entity" v-bind="$attrs" :module="dataKey">
<template v-for="(_, slotName) in $slots" #[slotName]="slotData" :key="slotName">
<slot :name="slotName" v-bind="slotData ?? {}" :key="slotName" />
</template>
</VnDescriptor>
</template>

View File

@ -1,32 +1,53 @@
<script setup>
defineProps({
hasImage: {
type: Boolean,
default: false,
},
});
</script>
<template>
<div id="descriptor-skeleton" class="bg-vn-page">
<div id="descriptor-skeleton">
<div class="row justify-between q-pa-sm">
<QSkeleton square size="30px" v-for="i in 3" :key="i" />
<QSkeleton square size="40px" />
<QSkeleton square size="40px" />
<QSkeleton square height="40px" width="20px" />
</div>
<div class="q-pa-xs" v-if="hasImage">
<QSkeleton square height="200px" width="100%" />
<div class="col justify-between q-pa-sm q-gutter-y-xs">
<QSkeleton square height="40px" width="150px" />
<QSkeleton square height="30px" width="70px" />
</div>
<div class="col justify-between q-pa-md q-gutter-y-xs">
<QSkeleton square height="25px" width="150px" />
<QSkeleton square height="15px" width="70px" />
</div>
<div class="q-pl-sm q-pa-sm q-mb-md">
<div class="row q-gutter-x-sm q-pa-none q-ma-none" v-for="i in 5" :key="i">
<QSkeleton type="text" square height="20px" width="30%" />
<QSkeleton type="text" square height="20px" width="60%" />
<div class="col q-pl-sm q-pa-sm q-mb-md">
<div class="row justify-between">
<QSkeleton type="text" square height="30px" width="20%" />
<QSkeleton type="text" square height="30px" width="60%" />
</div>
<div class="row justify-between">
<QSkeleton type="text" square height="30px" width="20%" />
<QSkeleton type="text" square height="30px" width="60%" />
</div>
<div class="row justify-between">
<QSkeleton type="text" square height="30px" width="20%" />
<QSkeleton type="text" square height="30px" width="60%" />
</div>
<div class="row justify-between">
<QSkeleton type="text" square height="30px" width="20%" />
<QSkeleton type="text" square height="30px" width="60%" />
</div>
<div class="row justify-between">
<QSkeleton type="text" square height="30px" width="20%" />
<QSkeleton type="text" square height="30px" width="60%" />
</div>
<div class="row justify-between">
<QSkeleton type="text" square height="30px" width="20%" />
<QSkeleton type="text" square height="30px" width="60%" />
</div>
</div>
<QCardActions class="q-gutter-x-sm justify-between">
<QSkeleton size="40px" v-for="i in 5" :key="i" />
<QCardActions>
<QSkeleton size="40px" />
<QSkeleton size="40px" />
<QSkeleton size="40px" />
<QSkeleton size="40px" />
<QSkeleton size="40px" />
</QCardActions>
</div>
</template>
<style lang="scss" scoped>
#descriptor-skeleton .q-card__actions {
justify-content: space-between;
}
</style>

View File

@ -82,7 +82,7 @@ function cancel() {
@click="cancel()"
/>
</QCardSection>
<QCardSection class="q-pb-none" data-cy="VnConfirm_message">
<QCardSection class="q-pb-none">
<span v-if="message !== false" v-html="message" />
</QCardSection>
<QCardSection class="row items-center q-pt-none">
@ -95,7 +95,6 @@ function cancel() {
:disable="isLoading"
flat
@click="cancel()"
data-cy="VnConfirm_cancel"
/>
<QBtn
:label="t('globals.confirm')"

View File

@ -1,318 +0,0 @@
<script setup>
import { computed, ref } from 'vue';
import { useI18n } from 'vue-i18n';
import SkeletonDescriptor from 'components/ui/SkeletonDescriptor.vue';
import { useSummaryDialog } from 'src/composables/useSummaryDialog';
import { useRoute, useRouter } from 'vue-router';
import { useClipboard } from 'src/composables/useClipboard';
import VnMoreOptions from './VnMoreOptions.vue';
const entity = defineModel({ type: Object, default: null });
const $props = defineProps({
title: {
type: String,
default: '',
},
subtitle: {
type: Number,
default: null,
},
summary: {
type: Object,
default: null,
},
width: {
type: String,
default: 'md-width',
},
module: {
type: String,
default: null,
},
toModule: {
type: Object,
default: null,
},
});
const route = useRoute();
const router = useRouter();
const { t } = useI18n();
const { copyText } = useClipboard();
const { viewSummary } = useSummaryDialog();
const DESCRIPTOR_PROXY = 'DescriptorProxy';
const moduleName = ref();
const isSameModuleName = route.matched[1].meta.moduleName !== moduleName.value;
function getName() {
let name = $props.module;
if ($props.module.includes(DESCRIPTOR_PROXY)) {
name = name.split(DESCRIPTOR_PROXY)[0];
}
return name;
}
const routeName = computed(() => {
let routeName = getName();
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: {
copyValue: id,
},
});
}
const emit = defineEmits(['onFetch']);
const iconModule = computed(() => {
moduleName.value = getName();
if ($props.toModule) {
return router.getRoutes().find((r) => r.name === $props.toModule.name).meta.icon;
}
if (isSameModuleName) {
return router.options.routes[1].children.find((r) => r.name === moduleName.value)
?.meta?.icon;
} else {
return route.matched[1].meta.icon;
}
});
const toModule = computed(() => {
moduleName.value = getName();
if ($props.toModule) return $props.toModule;
if (isSameModuleName) {
return router.options.routes[1].children.find((r) => r.name === moduleName.value)
?.redirect;
} else {
return route.matched[1].path.split('/').length > 2
? route.matched[1].redirect
: route.matched[1].children[0].redirect;
}
});
</script>
<template>
<div class="descriptor" data-cy="vnDescriptor">
<template v-if="entity && entity?.id">
<div class="header bg-primary q-pa-sm justify-between">
<slot name="header-extra-action">
<QBtn
round
flat
dense
size="md"
:icon="iconModule"
color="white"
class="link"
:to="toModule"
>
<QTooltip>
{{ t('globals.goToModuleIndex') }}
</QTooltip>
</QBtn>
</slot>
<QBtn
@click.stop="viewSummary(entity.id, summary, width)"
round
flat
dense
size="md"
icon="preview"
color="white"
class="link"
v-if="summary"
data-cy="openSummaryBtn"
>
<QTooltip>
{{ t('components.smartCard.openSummary') }}
</QTooltip>
</QBtn>
<RouterLink :to="{ name: routeName, params: { id: entity.id } }">
<QBtn
class="link"
color="white"
dense
flat
icon="launch"
round
size="md"
data-cy="goToSummaryBtn"
>
<QTooltip>
{{ t('components.vnDescriptor.summary') }}
</QTooltip>
</QBtn>
</RouterLink>
<VnMoreOptions v-if="$slots.menu">
<template #menu="{ menuRef }">
<slot name="menu" :entity="entity" :menu-ref="menuRef" />
</template>
</VnMoreOptions>
</div>
<slot name="before" />
<div class="body q-py-sm">
<QList dense>
<QItemLabel header class="ellipsis text-h5" :lines="1">
<div class="title">
<span
v-if="title"
:title="getValueFromPath(title)"
:data-cy="`${$attrs['data-cy'] ?? 'vnDescriptor'}_title`"
>
{{ getValueFromPath(title) ?? title }}
</span>
<slot v-else name="description" :entity="entity">
<span
:title="entity.name"
:data-cy="`${$attrs['data-cy'] ?? 'vnDescriptor'}_description`"
v-text="entity.name"
/>
</slot>
</div>
</QItemLabel>
<QItem>
<QItemLabel
class="subtitle"
:data-cy="`${$attrs['data-cy'] ?? 'vnDescriptor'}_subtitle`"
>
#{{ getValueFromPath(subtitle) ?? entity.id }}
</QItemLabel>
<QBtn
round
flat
dense
size="sm"
icon="content_copy"
color="primary"
@click.stop="copyIdText(entity.id)"
>
<QTooltip>
{{ t('globals.copyId') }}
</QTooltip>
</QBtn>
</QItem>
</QList>
<div
class="list-box q-mt-xs"
:data-cy="`${$attrs['data-cy'] ?? 'vnDescriptor'}_listbox`"
>
<slot name="body" :entity="entity" />
</div>
</div>
<div class="icons">
<slot name="icons" :entity="entity" />
</div>
<div class="actions justify-center" data-cy="descriptor_actions">
<slot name="actions" :entity="entity" />
</div>
<slot name="after" />
</template>
<SkeletonDescriptor v-if="!entity" />
</div>
<QInnerLoading :label="t('globals.pleaseWait')" :showing="!entity" color="primary" />
</template>
<style lang="scss">
.body {
background-color: var(--vn-section-color);
.text-h5 {
font-size: 20px;
padding-top: 5px;
padding-bottom: 0px;
}
.q-item {
min-height: 20px;
.link {
margin-left: 10px;
}
}
.vn-label-value {
display: flex;
padding: 0px 16px;
.label {
color: var(--vn-label-color);
font-size: 14px;
&:not(:has(a))::after {
content: ':';
}
}
.value {
color: var(--vn-text-color);
font-size: 14px;
margin-left: 4px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
text-align: left;
}
.info {
margin-left: 5px;
}
}
}
</style>
<style lang="scss" scoped>
.title {
overflow: hidden;
text-overflow: ellipsis;
span {
color: var(--vn-text-color);
font-weight: bold;
}
}
.subtitle {
color: var(--vn-text-color);
font-size: 16px;
margin-bottom: 2px;
}
.list-box {
.q-item__label {
color: var(--vn-label-color);
padding-bottom: 0%;
}
}
.descriptor {
width: 256px;
.header {
display: flex;
align-items: center;
}
.icons {
margin: 0 10px;
display: flex;
justify-content: center;
.q-icon {
margin-right: 5px;
}
}
.actions {
margin: 0 5px;
justify-content: center !important;
}
}
</style>
<i18n>
en:
globals:
copyId: Copy ID
es:
globals:
copyId: Copiar ID
</i18n>

View File

@ -54,17 +54,13 @@ const $props = defineProps({
default: 'table',
},
redirect: {
type: [String, Boolean],
type: Boolean,
default: true,
},
arrayData: {
type: Object,
default: null,
},
showTagChips: {
type: Boolean,
default: true,
},
});
const emit = defineEmits([
@ -92,14 +88,13 @@ const userOrders = ref(useFilterParams($props.dataKey).orders);
defineExpose({ search, params: userParams, remove });
const isLoading = ref(false);
async function search(evt, name, value) {
async function search(evt) {
try {
if (evt && $props.disableSubmitEvent) return;
store.filter.where = {};
isLoading.value = true;
const filter = { ...userParams.value, ...$props.modelValue };
if (name) filter[name] = value;
store.userParamsChanged = true;
await arrayData.addFilter({
params: filter,
@ -219,7 +214,7 @@ const getLocale = (label) => {
</QTooltip>
</QBtn>
<QForm @submit="search" id="filterPanelForm" @keyup.enter="search()">
<QList dense v-if="showTagChips">
<QList dense>
<QItem class="q-mt-xs">
<QItemSection top>
<QItemLabel header lines="1" class="text-uppercase q-py-xs q-px-none">
@ -254,7 +249,7 @@ const getLocale = (label) => {
:key="chip.label"
:removable="!unremovableParams?.includes(chip.label)"
@remove="remove(chip.label)"
:data-cy="`vnFilterPanelChip_${chip.label}`"
data-cy="vnFilterPanelChip"
>
<slot
name="tags"
@ -298,9 +293,6 @@ const getLocale = (label) => {
/>
</template>
<style scoped lang="scss">
.q-field__label.no-pointer-events.absolute.ellipsis {
margin-left: 6px !important;
}
.list {
width: 256px;
}

View File

@ -1,11 +1,8 @@
<script setup>
import { dashIfEmpty } from 'src/filters';
defineProps({ email: { type: [String], default: null } });
</script>
<template>
<QBtn
class="q-pr-xs"
v-if="email"
flat
round
@ -16,5 +13,4 @@ defineProps({ email: { type: [String], default: null } });
:href="`mailto:${email}`"
@click.stop
/>
<span>{{ dashIfEmpty(email) }}</span>
</template>

View File

@ -1,7 +1,7 @@
<script setup>
import { ref, reactive, useAttrs, onBeforeMount, capitalize } from 'vue';
import axios from 'axios';
import { dashIfEmpty, parsePhone } from 'src/filters';
import { parsePhone } from 'src/filters';
import useOpenURL from 'src/composables/useOpenURL';
const props = defineProps({
@ -12,65 +12,49 @@ const props = defineProps({
const phone = ref(props.phoneNumber);
const config = reactive({
sip: { icon: 'phone', href: `sip:${props.phoneNumber}` },
'say-simple': {
icon: 'vn:saysimple',
url: null,
channel: props.channel,
},
sip: { icon: 'phone', href: `sip:${props.phoneNumber}` },
});
const attrs = useAttrs();
const types = Object.keys(config)
.filter((key) => key in attrs)
.sort();
const activeTypes = types.length ? types : ['sip'];
const type = Object.keys(config).find((key) => key in useAttrs()) || 'sip';
onBeforeMount(async () => {
if (!phone.value) return;
let { channel } = config[type];
for (const type of activeTypes) {
if (type === 'say-simple') {
let { channel } = config[type];
const { url, defaultChannel } = (await axios.get('SaySimpleConfigs/findOne'))
.data;
if (!channel) channel = defaultChannel;
if (type === 'say-simple') {
const { url, defaultChannel } = (await axios.get('SaySimpleConfigs/findOne'))
.data;
if (!channel) channel = defaultChannel;
phone.value = await parsePhone(
props.phoneNumber,
props.country?.toLowerCase(),
);
config[type].url =
`${url}?customerIdentity=%2B${phone.value}&channelId=${channel}`;
}
phone.value = await parsePhone(props.phoneNumber, props.country?.toLowerCase());
config[
type
].url = `${url}?customerIdentity=%2B${phone.value}&channelId=${channel}`;
}
});
function handleClick(type) {
function handleClick() {
if (config[type].url) useOpenURL(config[type].url);
else if (config[type].href) window.location.href = config[type].href;
}
</script>
<template>
<div class="flex items-center gap-2">
<template v-for="type in activeTypes">
<QBtn
:key="type"
v-if="phone"
flat
round
:icon="config[type].icon"
size="sm"
color="primary"
padding="none"
@click.stop="() => handleClick(type)"
>
<QTooltip>
{{ capitalize(type).replace('-', '') }}
</QTooltip>
</QBtn></template
>
<span>{{ dashIfEmpty(phone) }}</span>
</div>
<QBtn
v-if="phone"
flat
round
:icon="config[type].icon"
size="sm"
color="primary"
padding="none"
@click.stop="handleClick"
>
<QTooltip>
{{ capitalize(type).replace('-', '') }}
</QTooltip>
</QBtn>
</template>

View File

@ -28,14 +28,13 @@ function copyValueText() {
const val = computed(() => $props.value);
</script>
<template>
<div class="vn-label-value" :data-cy="`${$attrs['data-cy'] ?? 'vnLv'}${label ?? ''}`">
<div class="vn-label-value">
<QCheckbox
v-if="typeof value === 'boolean'"
v-model="val"
:label="label"
disable
dense
size="sm"
/>
<template v-else>
<div v-if="label || $slots.label" class="label">
@ -43,9 +42,9 @@ const val = computed(() => $props.value);
<span style="color: var(--vn-label-color)">{{ label }}</span>
</slot>
</div>
<div class="value" v-if="value || $slots.value">
<div class="value">
<slot name="value">
<span :title="value" style="text-overflow: ellipsis">
<span :title="value">
{{ dash ? dashIfEmpty(value) : value }}
</span>
</slot>

View File

@ -9,10 +9,10 @@
data-cy="descriptor-more-opts"
>
<QTooltip>
{{ $t('components.vnDescriptor.moreOptions') }}
{{ $t('components.cardDescriptor.moreOptions') }}
</QTooltip>
<QMenu ref="menuRef" data-cy="descriptor-more-opts-menu">
<QList data-cy="descriptor-more-opts_list">
<QMenu ref="menuRef">
<QList>
<slot name="menu" :menu-ref="$refs.menuRef" />
</QList>
</QMenu>

View File

@ -18,16 +18,15 @@ import VnInput from 'components/common/VnInput.vue';
const emit = defineEmits(['onFetch']);
const originalAttrs = useAttrs();
const $attrs = computed(() => {
const { required, deletable, ...rest } = originalAttrs;
return rest;
const $attrs = useAttrs();
const isRequired = computed(() => {
return Object.keys($attrs).includes('required')
});
const $props = defineProps({
url: { type: String, default: null },
saveUrl: { type: String, default: null },
userFilter: { type: Object, default: () => {} },
saveUrl: {type: String, default: null},
filter: { type: Object, default: () => {} },
body: { type: Object, default: () => {} },
addNote: { type: Boolean, default: false },
@ -40,11 +39,6 @@ const quasar = useQuasar();
const newNote = reactive({ text: null, observationTypeFk: null });
const observationTypes = ref([]);
const vnPaginateRef = ref();
const defaultObservationType = computed(() =>
observationTypes.value.find(ot => ot.code === 'salesPerson')?.id
);
let originalText;
function handleClick(e) {
@ -53,11 +47,6 @@ function handleClick(e) {
else insert();
}
async function deleteNote(e) {
await axios.delete(`${$props.url}/${e.id}`);
await vnPaginateRef.value.fetch();
}
async function insert() {
if (!newNote.text || ($props.selectType && !newNote.observationTypeFk)) return;
@ -71,7 +60,7 @@ async function insert() {
}
function confirmAndUpdate() {
if (!newNote.text && originalText)
if(!newNote.text && originalText)
quasar
.dialog({
component: VnConfirm,
@ -94,17 +83,11 @@ async function update() {
...body,
...{ notes: newNote.text },
};
await axios.patch(
`${$props.saveUrl ?? `${$props.url}/${$props.body.workerFk}`}`,
newBody,
);
await axios.patch(`${$props.saveUrl ?? `${$props.url}/${$props.body.workerFk}`}`, newBody);
}
onBeforeRouteLeave((to, from, next) => {
if (
(newNote.text && !$props.justInput) ||
(newNote.text !== originalText && $props.justInput)
)
if ((newNote.text && !$props.justInput) || (newNote.text !== originalText) && $props.justInput)
quasar.dialog({
component: VnConfirm,
componentProps: {
@ -116,27 +99,20 @@ onBeforeRouteLeave((to, from, next) => {
else next();
});
function fetchData([data]) {
function fetchData([ data ]) {
newNote.text = data?.notes;
originalText = data?.notes;
emit('onFetch', data);
}
const handleObservationTypes = (data) => {
observationTypes.value = data;
if(defaultObservationType.value) {
newNote.observationTypeFk = defaultObservationType.value;
}
};
</script>
<template>
<FetchData
v-if="selectType"
url="ObservationTypes"
:filter="{ fields: ['id', 'description', 'code'] }"
:filter="{ fields: ['id', 'description'] }"
auto-load
@on-fetch="handleObservationTypes"
@on-fetch="(data) => (observationTypes = data)"
/>
<FetchData
v-if="justInput"
@ -145,8 +121,8 @@ const handleObservationTypes = (data) => {
@on-fetch="fetchData"
auto-load
/>
<QCard
class="q-pa-xs q-mb-lg full-width"
<QCard
class="q-pa-xs q-mb-lg full-width"
:class="{ 'just-input': $props.justInput }"
v-if="$props.addNote || $props.justInput"
>
@ -162,7 +138,7 @@ const handleObservationTypes = (data) => {
v-model="newNote.observationTypeFk"
option-label="description"
style="flex: 0.15"
:required="'required' in originalAttrs"
:required="isRequired"
@keyup.enter.stop="insert"
/>
<VnInput
@ -170,10 +146,10 @@ const handleObservationTypes = (data) => {
type="textarea"
:label="$props.justInput && newNote.text ? '' : t('Add note here...')"
filled
size="lg"
autogrow
autofocus
@keyup.enter.stop="handleClick"
:required="'required' in originalAttrs"
:required="isRequired"
clearable
>
<template #append>
@ -198,15 +174,15 @@ const handleObservationTypes = (data) => {
:url="$props.url"
order="created DESC"
:limit="0"
:user-filter="userFilter"
:filter="filter"
:user-filter="$props.filter"
auto-load
ref="vnPaginateRef"
class="show"
v-bind="$attrs"
:search-url="false"
search-url="notes"
@on-fetch="
newNote.text = '';
newNote.observationTypeFk = null;
"
>
<template #body="{ rows }">
@ -237,27 +213,12 @@ const handleObservationTypes = (data) => {
>
{{
observationTypes.find(
(ot) => ot.id === note.observationTypeFk,
(ot) => ot.id === note.observationTypeFk
)?.description
}}
</QBadge>
</div>
<span v-text="toDateHourMin(note.created)" />
<div>
<QIcon
v-if="'deletable' in originalAttrs"
name="delete"
size="sm"
class="cursor-pointer"
color="primary"
@click="deleteNote(note)"
data-cy="notesRemoveNoteBtn"
>
<QTooltip>
{{ t('ticketNotes.removeNote') }}
</QTooltip>
</QIcon>
</div>
</div>
</QCardSection>
<QCardSection class="q-pa-xs q-my-none q-py-none">

View File

@ -115,7 +115,7 @@ onMounted(async () => {
});
onBeforeUnmount(() => {
arrayData.reset(['data']);
if (!store.keepData) arrayData.reset(['data']);
arrayData.resetPagination();
});
@ -215,7 +215,6 @@ defineExpose({
paginate,
userParams: arrayData.store.userParams,
currentFilter: arrayData.store.currentFilter,
arrayData,
});
</script>

View File

@ -33,10 +33,6 @@ const props = defineProps({
type: String,
default: '',
},
userFilter: {
type: Object,
default: null,
},
filter: {
type: Object,
default: null,
@ -208,9 +204,8 @@ async function search() {
}
:deep(.q-field--focused) {
.q-icon,
.q-placeholder {
color: var(--vn-black-text-color);
.q-icon {
color: black;
}
}

View File

@ -1,41 +0,0 @@
<script setup>
import { toPercentage } from 'filters/index';
import { computed } from 'vue';
const props = defineProps({
value: {
type: Number,
required: true,
},
});
const valueClass = computed(() =>
props.value === 0 ? 'neutral' : props.value > 0 ? 'positive' : 'negative',
);
const iconName = computed(() =>
props.value === 0 ? 'equal' : props.value > 0 ? 'arrow_upward' : 'arrow_downward',
);
const formattedValue = computed(() => props.value);
</script>
<template>
<span :class="valueClass">
<QIcon :name="iconName" size="sm" class="value-icon" />
{{ toPercentage(formattedValue) }}
</span>
</template>
<style lang="scss" scoped>
.positive {
color: $secondary;
}
.negative {
color: $negative;
}
.neutral {
color: $primary;
}
.value-icon {
margin-right: 4px;
}
</style>

View File

@ -26,7 +26,6 @@ const id = props.entityId;
:to="{ name: routeName, params: { id: id } }"
class="header link"
:href="url"
data-cy="goToSummaryBtn"
>
<QIcon name="open_in_new" color="white" size="sm" />
</router-link>

View File

@ -53,8 +53,3 @@ const manaCode = ref(props.manaCode);
/>
</div>
</template>
<i18n>
es:
Promotion mana: Maná promoción
Claim mana: Maná reclamación
</i18n>

View File

@ -6,12 +6,10 @@ const session = useSession();
const token = session.getToken();
describe('downloadFile', () => {
const baseUrl = 'http://localhost:9000';
let defaulCreateObjectURL;
beforeAll(() => {
vi.mock('src/composables/getUrl', () => ({
getUrl: vi.fn().mockResolvedValue(''),
}));
defaulCreateObjectURL = window.URL.createObjectURL;
window.URL.createObjectURL = vi.fn(() => 'blob:http://localhost:9000/blob-id');
});
@ -24,14 +22,15 @@ describe('downloadFile', () => {
headers: { 'content-disposition': 'attachment; filename="test-file.txt"' },
};
vi.spyOn(axios, 'get').mockImplementation((url) => {
if (url.includes('downloadFile')) return Promise.resolve(res);
if (url == 'Urls/getUrl') return Promise.resolve({ data: baseUrl });
else if (url.includes('downloadFile')) return Promise.resolve(res);
});
await downloadFile(1);
expect(axios.get).toHaveBeenCalledWith(
`/api/dms/1/downloadFile?access_token=${token}`,
{ responseType: 'blob' },
`${baseUrl}/api/dms/1/downloadFile?access_token=${token}`,
{ responseType: 'blob' }
);
});
});

View File

@ -1,66 +0,0 @@
import { describe, it, expect, vi } from 'vitest';
import { useRequired } from '../useRequired';
vi.mock('../useValidator', () => ({
useValidator: () => ({
validations: () => ({
required: vi.fn((isRequired, val) => {
if (!isRequired) return true;
return val !== null && val !== undefined && val !== '';
}),
}),
}),
}));
describe('useRequired', () => {
it('should detect required when attr is boolean true', () => {
const attrs = { required: true };
const { isRequired } = useRequired(attrs);
expect(isRequired).toBe(true);
});
it('should detect required when attr is boolean false', () => {
const attrs = { required: false };
const { isRequired } = useRequired(attrs);
expect(isRequired).toBe(false);
});
it('should detect required when attr exists without value', () => {
const attrs = { required: '' };
const { isRequired } = useRequired(attrs);
expect(isRequired).toBe(true);
});
it('should return false when required attr does not exist', () => {
const attrs = { someOtherAttr: 'value' };
const { isRequired } = useRequired(attrs);
expect(isRequired).toBe(false);
});
describe('requiredFieldRule', () => {
it('should validate required field with value', () => {
const attrs = { required: true };
const { requiredFieldRule } = useRequired(attrs);
expect(requiredFieldRule('some value')).toBe(true);
});
it('should validate required field with empty value', () => {
const attrs = { required: true };
const { requiredFieldRule } = useRequired(attrs);
expect(requiredFieldRule('')).toBe(false);
});
it('should pass validation when field is not required', () => {
const attrs = { required: false };
const { requiredFieldRule } = useRequired(attrs);
expect(requiredFieldRule('')).toBe(true);
});
it('should handle null and undefined values', () => {
const attrs = { required: true };
const { requiredFieldRule } = useRequired(attrs);
expect(requiredFieldRule(null)).toBe(false);
expect(requiredFieldRule(undefined)).toBe(false);
});
});
});

View File

@ -23,19 +23,18 @@ describe('useRole', () => {
name: `T'Challa`,
nickname: 'Black Panther',
lang: 'en',
worker: { department: { departmentFk: 155 } },
};
const expectedUser = {
id: 999,
name: `T'Challa`,
nickname: 'Black Panther',
lang: 'en',
departmentFk: 155,
};
const expectedRoles = ['salesPerson', 'admin'];
vi.spyOn(axios, 'get').mockResolvedValueOnce({
vi.spyOn(axios, 'get')
.mockResolvedValueOnce({
data: { roles: rolesData, user: fetchedUser },
});
})
vi.spyOn(role.state, 'setUser');
vi.spyOn(role.state, 'setRoles');

View File

@ -75,7 +75,6 @@ describe('session', () => {
userConfig: {
darkMode: false,
},
worker: { department: { departmentFk: 155 } },
};
const rolesData = [
{
@ -144,7 +143,7 @@ describe('session', () => {
await session.destroy(); // this clears token and user for any other test
});
},
{},
{}
);
describe('RenewToken', () => {
@ -176,7 +175,7 @@ describe('session', () => {
await session.checkValidity();
expect(sessionStorage.getItem('token')).toEqual(expectedToken);
expect(sessionStorage.getItem('tokenMultimedia')).toEqual(
expectedTokenMultimedia,
expectedTokenMultimedia
);
});
it('Should renewToken', async () => {
@ -205,7 +204,7 @@ describe('session', () => {
await session.checkValidity();
expect(sessionStorage.getItem('token')).not.toEqual(expectedToken);
expect(sessionStorage.getItem('tokenMultimedia')).not.toEqual(
expectedTokenMultimedia,
expectedTokenMultimedia
);
});
});

View File

@ -1,64 +0,0 @@
import { useQuasar } from 'quasar';
import { useI18n } from 'vue-i18n';
import { useRouter } from 'vue-router';
import axios from 'axios';
import VnConfirm from 'components/ui/VnConfirm.vue';
export async function checkEntryLock(entryFk, userFk) {
const { t } = useI18n();
const quasar = useQuasar();
const { push } = useRouter();
const { data } = await axios.get(`Entries/${entryFk}`, {
params: {
filter: JSON.stringify({
fields: ['id', 'locked', 'lockerUserFk'],
include: { relation: 'user', scope: { fields: ['id', 'nickname'] } },
}),
},
});
const entryConfig = await axios.get('EntryConfigs/findOne');
if (data?.lockerUserFk && data?.locked) {
const now = new Date(Date.vnNow()).getTime();
const lockedTime = new Date(data.locked).getTime();
const timeDiff = (now - lockedTime) / 1000;
const isMaxTimeLockExceeded = entryConfig.data.maxLockTime > timeDiff;
if (data?.lockerUserFk !== userFk && isMaxTimeLockExceeded) {
quasar
.dialog({
component: VnConfirm,
componentProps: {
title: t('entry.lock.title'),
message: t('entry.lock.message', {
userName: data?.user?.nickname,
time: timeDiff / 60,
}),
},
})
.onOk(
async () =>
await axios.patch(`Entries/${entryFk}`, {
locked: Date.vnNow(),
lockerUserFk: userFk,
}),
)
.onCancel(() => {
push({ path: `summary` });
});
}
} else {
await axios
.patch(`Entries/${entryFk}`, {
locked: Date.vnNow(),
lockerUserFk: userFk,
})
.then(
quasar.notify({
message: t('entry.lock.success'),
color: 'positive',
group: false,
}),
);
}
}

View File

@ -7,33 +7,18 @@ const { getTokenMultimedia } = useSession();
const token = getTokenMultimedia();
export async function downloadFile(id, model = 'dms', urlPath = '/downloadFile', url) {
const appUrl = await getAppUrl();
const appUrl = (await getUrl('', 'lilium')).replace('/#/', '');
const response = await axios.get(
url ?? `${appUrl}/api/${model}/${id}${urlPath}?access_token=${token}`,
{ responseType: 'blob' },
{ responseType: 'blob' }
);
download(response);
}
export async function downloadDocuware(url, params) {
const appUrl = await getAppUrl();
const response = await axios.get(`${appUrl}/api/` + url, {
responseType: 'blob',
params,
});
download(response);
}
function download(response) {
const contentDisposition = response.headers['content-disposition'];
const matches = /filename[^;=\n]*=((['"]).*?\2|[^;\n]*)/.exec(contentDisposition);
const filename = matches?.[1] ? matches[1].replace(/['"]/g, '') : 'downloaded-file';
const filename =
matches != null && matches[1]
? matches[1].replace(/['"]/g, '')
: 'downloaded-file';
exportFile(filename, response.data);
}
async function getAppUrl() {
return (await getUrl('', 'lilium')).replace('/#/', '');
}

View File

@ -1,24 +0,0 @@
export function getColAlign(col) {
let align;
switch (col.component) {
case 'time':
case 'date':
case 'select':
align = 'left';
break;
case 'number':
align = 'right';
break;
case 'time':
case 'date':
case 'checkbox':
align = 'center';
break;
default:
align = col?.align;
}
if (/^is[A-Z]/.test(col.name) || /^has[A-Z]/.test(col.name)) align = 'center';
return 'text-' + (align ?? 'center');
}

View File

@ -30,16 +30,9 @@ export function useAcl() {
return false;
}
function hasAcl(model, prop, accessType) {
const modelAcl = state.getAcls().value[model];
const propAcl = modelAcl?.[prop] || modelAcl?.['*'];
return !!(propAcl?.[accessType] || propAcl?.['*']);
}
return {
fetch,
hasAny,
state,
hasAcl,
};
}

View File

@ -56,6 +56,7 @@ export function useArrayData(key, userOptions) {
'searchUrl',
'navigate',
'mapKey',
'keepData',
'oneRecord',
];
if (typeof userOptions === 'object') {
@ -107,7 +108,7 @@ export function useArrayData(key, userOptions) {
store.hasMoreData = limit && response.data.length >= limit;
if (!append && !isDialogOpened() && updateRouter) {
if (updateStateParams(response.data)?.redirect) return;
if (updateStateParams(response.data)?.redirect && !store.keepData) return;
}
store.isLoading = false;
canceller = null;
@ -147,7 +148,8 @@ export function useArrayData(key, userOptions) {
}
async function applyFilter({ filter, params }, fetchOptions = {}) {
if (filter) store.filter = filter;
if (filter) store.userFilter = filter;
store.filter = {};
if (params) store.userParams = { ...params };
const response = await fetch(fetchOptions);
@ -188,7 +190,7 @@ export function useArrayData(key, userOptions) {
store.order = order;
resetPagination();
await fetch({});
fetch({});
index++;
return { index, order };
@ -243,7 +245,7 @@ export function useArrayData(key, userOptions) {
async function loadMore() {
if (!store.hasMoreData) return;
store.skip = (store?.filter?.limit ?? store.limit) * store.page;
store.skip = store.limit * store.page;
store.page += 1;
await fetch({ append: true });

View File

@ -11,7 +11,6 @@ export async function useCau(res, message) {
const { config, headers, request, status, statusText, data } = res || {};
const { params, url, method, signal, headers: confHeaders } = config || {};
const { message: resMessage, code, name } = data?.error || {};
delete confHeaders?.Authorization;
const additionalData = {
path: location.hash,
@ -41,7 +40,7 @@ export async function useCau(res, message) {
handler: async () => {
const locale = i18n.global.t;
const reason = ref(
code == 'ACCESS_DENIED' ? locale('cau.askPrivileges') : '',
code == 'ACCESS_DENIED' ? locale('cau.askPrivileges') : ''
);
openConfirmationModal(
locale('cau.title'),
@ -60,9 +59,10 @@ export async function useCau(res, message) {
'onUpdate:modelValue': (val) => (reason.value = val),
label: locale('cau.inputLabel'),
class: 'full-width',
required: true,
autofocus: true,
},
},
}
);
},
},

Some files were not shown because too many files have changed in this diff Show More