diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 000000000..3c3629e64 --- /dev/null +++ b/.dockerignore @@ -0,0 +1 @@ +node_modules diff --git a/Jenkinsfile b/Jenkinsfile index c20da8ab2..341fffefa 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -1,6 +1,7 @@ #!/usr/bin/env groovy def PROTECTED_BRANCH +def IS_LATEST def BRANCH_ENV = [ test: 'test', @@ -10,16 +11,18 @@ 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_LATEST = ['master', 'main'].contains(env.BRANCH_NAME) + // https://www.jenkins.io/doc/book/pipeline/jenkinsfile/#using-environment-variables echo "NODE_NAME: ${env.NODE_NAME}" echo "WORKSPACE: ${env.WORKSPACE}" @@ -58,6 +61,19 @@ pipeline { PROJECT_NAME = 'lilium' } stages { + stage('Version') { + when { + expression { 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 = "" @@ -71,17 +87,48 @@ pipeline { expression { !PROTECTED_BRANCH } } environment { - NODE_ENV = "" + NODE_ENV = '' + CI = 'true' + TZ = 'Europe/Madrid' } - steps { - sh 'pnpm run test:unit:ci' - } - post { - always { - junit( - testResults: 'junitresults.xml', - allowEmptyResults: true - ) + parallel { + stage('Unit') { + steps { + sh 'pnpm run test:unit:ci' + } + post { + always { + junit( + testResults: 'junit/vitest.xml', + allowEmptyResults: true + ) + } + } + } + stage('E2E') { + environment { + CREDENTIALS = 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 { + def image = docker.build('lilium-dev', '-f docs/Dockerfile.dev docs') + sh "docker-compose ${env.COMPOSE_PARAMS} up -d" + image.inside("--network ${env.COMPOSE_PROJECT}_default -e CI -e TZ") { + sh 'cypress run --browser chromium' + } + } + } + post { + always { + sh "docker-compose ${env.COMPOSE_PARAMS} down -v" + junit( + testResults: 'junit/e2e.xml', + allowEmptyResults: true + ) + } + } } } } @@ -91,25 +138,30 @@ pipeline { } environment { CREDENTIALS = credentials('docker-registry') + VERSION = readFile 'VERSION.txt' } steps { - sh 'quasar build' script { - def packageJson = readJSON file: 'package.json' - env.VERSION = "${packageJson.version}-build${env.BUILD_ID}" + 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') + } } - dockerBuild() } } stage('Deploy') { when { expression { PROTECTED_BRANCH } } + environment { + VERSION = readFile 'VERSION.txt' + } steps { - script { - def packageJson = readJSON file: 'package.json' - env.VERSION = "${packageJson.version}-build${env.BUILD_ID}" - } withKubeConfig([ serverUrl: "$KUBERNETES_API", credentialsId: 'kubernetes', diff --git a/cypress.config.js b/cypress.config.js index 26b7725a5..dfe963a12 100644 --- a/cypress.config.js +++ b/cypress.config.js @@ -1,12 +1,37 @@ import { defineConfig } from 'cypress'; -// https://docs.cypress.io/app/tooling/reporters -// https://docs.cypress.io/app/references/configuration -// https://www.npmjs.com/package/cypress-mochawesome-reporter + +let urlHost, reporter, reporterOptions; + +if (process.env.CI) { + urlHost = 'front'; + reporter = 'junit'; + reporterOptions = { + mochaFile: 'junit/e2e.xml', + toConsole: false, + }; +} 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, + }; +} export default defineConfig({ e2e: { - baseUrl: 'http://localhost:9000/', - experimentalStudio: true, + baseUrl: `http://${urlHost}:9000`, + experimentalStudio: false, + defaultCommandTimeout: 10000, + trashAssetsBeforeRuns: false, + requestTimeout: 10000, + responseTimeout: 30000, + pageLoadTimeout: 60000, + defaultBrowser: 'chromium', fixturesFolder: 'test/cypress/fixtures', screenshotsFolder: 'test/cypress/screenshots', supportFile: 'test/cypress/support/index.js', @@ -14,40 +39,19 @@ export default defineConfig({ downloadsFolder: 'test/cypress/downloads', video: false, specPattern: 'test/cypress/integration/**/*.spec.js', - experimentalRunAllSpecs: false, - watchForFileChanges: false, - reporter: 'cypress-mochawesome-reporter', - reporterOptions: { - charts: true, - reportPageTitle: 'Cypress Inline Reporter', - reportFilename: '[status]_[datetime]-report', - embeddedScreenshots: true, - reportDir: 'test/cypress/reports', - inlineAssets: true, - }, + experimentalRunAllSpecs: true, + watchForFileChanges: true, + reporter, + reporterOptions, 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); - - const fs = await import('fs'); - on('task', { - deleteFile(filePath) { - if (fs.existsSync(filePath)) { - fs.unlinkSync(filePath); - return true; - } - return false; - }, - }); - - return config; - }, viewportWidth: 1280, viewportHeight: 720, }, + experimentalMemoryManagement: true, + defaultCommandTimeout: 10000, + numTestsKeptInMemory: 2, }); diff --git a/docker-compose.yml b/docker-compose.yml deleted file mode 100644 index df793fc75..000000000 --- a/docker-compose.yml +++ /dev/null @@ -1,7 +0,0 @@ -version: '3.7' -services: - main: - image: registry.verdnatura.es/salix-frontend:${VERSION:?} - build: - context: . - dockerfile: ./Dockerfile diff --git a/docs/Dockerfile.dev b/docs/Dockerfile.dev new file mode 100644 index 000000000..29b194ffa --- /dev/null +++ b/docs/Dockerfile.dev @@ -0,0 +1,45 @@ +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 \ + 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@13.6.6 \ + && cypress install + +WORKDIR /app diff --git a/proxy-serve.js b/proxy-serve.js index 415968c85..1e9bcf96b 100644 --- a/proxy-serve.js +++ b/proxy-serve.js @@ -1,6 +1,6 @@ export default [ { path: '/api', - rule: { target: 'http://0.0.0.0:3000' }, + rule: { target: 'http://127.0.0.1:3000' }, }, ]; diff --git a/quasar.config.js b/quasar.config.js index 9467c92af..8b6125a90 100644 --- a/quasar.config.js +++ b/quasar.config.js @@ -11,6 +11,7 @@ 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 { @@ -108,13 +109,17 @@ export default configure(function (/* ctx */) { }, proxy: { '/api': { - target: 'http://0.0.0.0:3000', + target: target, 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 diff --git a/src/components/FormModel.vue b/src/components/FormModel.vue index 04ef13d45..182eeaafe 100644 --- a/src/components/FormModel.vue +++ b/src/components/FormModel.vue @@ -12,6 +12,7 @@ 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(); @@ -284,7 +285,12 @@ 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; @@ -321,6 +327,7 @@ defineExpose({ class="q-pa-md" :style="maxWidth ? 'max-width: ' + maxWidth : ''" id="formModel" + :mapper="onBeforeSave" > -import { ref, computed } from 'vue'; +import { ref, computed, useAttrs, nextTick } from 'vue'; import { useI18n } from 'vue-i18n'; +import { useState } from 'src/composables/useState'; import FormModel from 'components/FormModel.vue'; const emit = defineEmits(['onDataSaved', 'onDataCanceled']); -defineProps({ +const props = defineProps({ title: { type: String, default: '', @@ -22,12 +23,21 @@ defineProps({ }); const { t } = useI18n(); - +const attrs = useAttrs(); +const state = useState(); const formModelRef = ref(null); const closeButton = ref(null); -const isSaveAndContinue = ref(false); -const onDataSaved = (formData, requestResponse) => { - if (closeButton.value && !isSaveAndContinue.value) closeButton.value.click(); +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; emit('onDataSaved', formData, requestResponse); }; @@ -36,9 +46,6 @@ const onClick = async (saveAndContinue) => { await formModelRef.value.save(); }; -const isLoading = computed(() => formModelRef.value?.isLoading); -const reset = computed(() => formModelRef.value?.reset); - defineExpose({ isLoading, onDataSaved, @@ -74,10 +81,7 @@ defineExpose({ data-cy="FormModelPopup_cancel" v-close-popup z-max - @click=" - isSaveAndContinue = false; - emit('onDataCanceled'); - " + @click="emit('onDataCanceled')" /> window.location.reload(); - + { if (isSelect && value) { - row[column.name] = value[column.attrs?.optionValue ?? 'id']; - row[column?.name + 'TextValue'] = - value[column.attrs?.optionLabel ?? 'name']; - await column?.cellEvent?.['update:modelValue']?.( - value, - oldValue, - row, - ); + await updateSelectValue(value, column, row, oldValue); } else row[column.name] = value; await column?.cellEvent?.['update:modelValue']?.(value, oldValue, row); }, keyup: async (event) => { if (event.key === 'Enter') - await destroyInput(rowIndex, field, clickedElement); + await destroyInput(rowId, field, clickedElement); }, keydown: async (event) => { switch (event.key) { @@ -457,6 +447,17 @@ async function renderInput(rowId, field, clickedElement) { node.el?.querySelector('span > div > div').focus(); } +async function updateSelectValue(value, column, row, oldValue) { + row[column.name] = value[column.attrs?.optionValue ?? 'id']; + + row[column?.name + 'VnTableTextValue'] = value[column.attrs?.optionLabel ?? 'name']; + + if (column?.attrs?.find?.label) + row[column?.attrs?.find?.label] = value[column.attrs?.optionLabel ?? 'name']; + + await column?.cellEvent?.['update:modelValue']?.(value, oldValue, row); +} + async function destroyInput(rowIndex, field, clickedElement) { if (!clickedElement) clickedElement = document.querySelector( @@ -519,9 +520,9 @@ function getToggleIcon(value) { } function formatColumnValue(col, row, dashIfEmpty) { - if (col?.format) { - if (selectRegex.test(col?.component) && row[col?.name + 'TextValue']) { - return dashIfEmpty(row[col?.name + 'TextValue']); + if (col?.format || row[col?.name + 'VnTableTextValue']) { + if (selectRegex.test(col?.component) && row[col?.name + 'VnTableTextValue']) { + return dashIfEmpty(row[col?.name + 'VnTableTextValue']); } else { return col.format(row, dashIfEmpty); } @@ -554,19 +555,33 @@ function formatColumnValue(col, row, dashIfEmpty) { } return dashIfEmpty(row[col?.name]); } + function cardClick(_, row) { if ($props.redirect) router.push({ path: `/${$props.redirect}/${row.id}` }); } + +function removeTextValue(data, getChanges) { + let changes = data.updates; + if (!changes) return data; + + for (const change of changes) { + for (const key in change.data) { + if (key.endsWith('VnTableTextValue')) { + delete change.data[key]; + } + } + } + + data.updates = changes.filter((change) => Object.keys(change.data).length > 0); + + if ($attrs?.beforeSaveFn) data = $attrs.beforeSaveFn(data, getChanges); + + return data; +} + { ); }); +const routeName = computed(() => { + const DESCRIPTOR_PROXY = 'DescriptorProxy'; + + let name = $props.dataKey; + if ($props.dataKey.includes(DESCRIPTOR_PROXY)) { + name = name.split(DESCRIPTOR_PROXY)[0]; + } + return `${name}Summary`; +}); async function getData() { store.url = $props.url; store.filter = $props.filter ?? {}; @@ -154,9 +163,7 @@ const toModule = computed(() => {{ t('components.smartCard.openSummary') }} - + #{{ getValueFromPath(subtitle) ?? entity.id }} - {{ t('globals.copyId') }} -
@@ -220,7 +225,7 @@ const toModule = computed(() =>
-
+
diff --git a/src/i18n/locale/en.yml b/src/i18n/locale/en.yml index 9a60e9da1..9e46c54e3 100644 --- a/src/i18n/locale/en.yml +++ b/src/i18n/locale/en.yml @@ -153,6 +153,7 @@ globals: maxTemperature: Max minTemperature: Min changePass: Change password + setPass: Set password deleteConfirmTitle: Delete selected elements changeState: Change state raid: 'Raid {daysInForward} days' diff --git a/src/i18n/locale/es.yml b/src/i18n/locale/es.yml index 846c442ea..fd42d37c7 100644 --- a/src/i18n/locale/es.yml +++ b/src/i18n/locale/es.yml @@ -157,6 +157,7 @@ globals: maxTemperature: Máx minTemperature: Mín changePass: Cambiar contraseña + setPass: Establecer contraseña deleteConfirmTitle: Eliminar los elementos seleccionados changeState: Cambiar estado raid: 'Redada {daysInForward} días' @@ -786,7 +787,7 @@ worker: notes: Notas operator: numberOfWagons: Número de vagones - train: tren + train: Tren itemPackingType: Tipo de embalaje warehouse: Almacén sector: Sector diff --git a/src/pages/Account/Card/AccountDescriptorMenu.vue b/src/pages/Account/Card/AccountDescriptorMenu.vue index 30584c61f..eafd62df6 100644 --- a/src/pages/Account/Card/AccountDescriptorMenu.vue +++ b/src/pages/Account/Card/AccountDescriptorMenu.vue @@ -25,12 +25,13 @@ const $props = defineProps({ const { t } = useI18n(); const { hasAccount } = toRefs($props); const { openConfirmationModal } = useVnConfirm(); +const arrayData = useArrayData('Account'); const route = useRoute(); const router = useRouter(); const state = useState(); const user = state.getUser(); const { notify } = useQuasar(); -const account = computed(() => useArrayData('Account').store.data[0]); +const account = computed(() => arrayData.store.data); account.value.hasAccount = hasAccount.value; const entityId = computed(() => +route.params.id); const hasitManagementAccess = ref(); @@ -39,7 +40,7 @@ const isHimself = computed(() => user.value.id === account.value.id); const url = computed(() => isHimself.value ? 'Accounts/change-password' - : `Accounts/${entityId.value}/setPassword` + : `Accounts/${entityId.value}/setPassword`, ); async function updateStatusAccount(active) { @@ -153,6 +154,7 @@ onMounted(() => { t('account.card.actions.disableAccount.title'), t('account.card.actions.disableAccount.subtitle'), () => deleteAccount(), + () => deleteAccount(), ) " > @@ -172,6 +174,7 @@ onMounted(() => { t('account.card.actions.enableAccount.title'), t('account.card.actions.enableAccount.subtitle'), () => updateStatusAccount(true), + () => updateStatusAccount(true), ) " > @@ -186,6 +189,7 @@ onMounted(() => { t('account.card.actions.disableAccount.title'), t('account.card.actions.disableAccount.subtitle'), () => updateStatusAccount(false), + () => updateStatusAccount(false), ) " > @@ -201,6 +205,7 @@ onMounted(() => { t('account.card.actions.activateUser.title'), t('account.card.actions.activateUser.title'), () => updateStatusUser(true), + () => updateStatusUser(true), ) " > @@ -215,6 +220,7 @@ onMounted(() => { t('account.card.actions.deactivateUser.title'), t('account.card.actions.deactivateUser.title'), () => updateStatusUser(false), + () => updateStatusUser(false), ) " > diff --git a/src/pages/Claim/Card/ClaimAction.vue b/src/pages/Claim/Card/ClaimAction.vue index 8ac7c224f..baa36710c 100644 --- a/src/pages/Claim/Card/ClaimAction.vue +++ b/src/pages/Claim/Card/ClaimAction.vue @@ -27,6 +27,7 @@ const claimActionsForm = ref(); const rows = ref([]); const selectedRows = ref([]); const destinationTypes = ref([]); +const shelvings = ref([]); const totalClaimed = ref(null); const DEFAULT_MAX_RESPONSABILITY = 5; const DEFAULT_MIN_RESPONSABILITY = 1; @@ -56,6 +57,12 @@ const columns = computed(() => [ field: (row) => row.claimDestinationFk, align: 'left', }, + { + name: 'shelving', + label: t('shelvings.shelving'), + field: (row) => row.shelvingFk, + align: 'left', + }, { name: 'Landed', label: t('Landed'), @@ -125,6 +132,10 @@ async function updateDestination(claimDestinationFk, row, options = {}) { options.reload && claimActionsForm.value.reload(); } } +async function updateShelving(shelvingFk, row) { + await axios.patch(`ClaimEnds/${row.id}`, { shelvingFk }); + claimActionsForm.value.reload(); +} async function regularizeClaim() { await post(`Claims/${claimId}/regularizeClaim`); @@ -200,6 +211,7 @@ async function post(query, params) { auto-load @on-fetch="(data) => (destinationTypes = data)" /> + + + diff --git a/src/pages/Claim/Card/ClaimBasicData.vue b/src/pages/Claim/Card/ClaimBasicData.vue index 67034da1a..43941d1dc 100644 --- a/src/pages/Claim/Card/ClaimBasicData.vue +++ b/src/pages/Claim/Card/ClaimBasicData.vue @@ -40,7 +40,7 @@ const workersOptions = ref([]);