forked from verdnatura/salix-front
Merge branch 'dev' of https://gitea.verdnatura.es/verdnatura/salix-front into test
This commit is contained in:
commit
592dc7da22
|
@ -13,13 +13,13 @@ pipeline {
|
||||||
steps {
|
steps {
|
||||||
script {
|
script {
|
||||||
switch (env.BRANCH_NAME) {
|
switch (env.BRANCH_NAME) {
|
||||||
// case 'master':
|
case 'master':
|
||||||
// env.NODE_ENV = 'production'
|
env.NODE_ENV = 'production'
|
||||||
// env.BACK_REPLICAS = 1
|
env.FRONT_REPLICAS = 2
|
||||||
// break
|
break
|
||||||
case 'test':
|
case 'test':
|
||||||
env.NODE_ENV = 'test'
|
env.NODE_ENV = 'test'
|
||||||
env.BACK_REPLICAS = 1
|
env.FRONT_REPLICAS = 1
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -58,7 +58,7 @@ pipeline {
|
||||||
stage('Build') {
|
stage('Build') {
|
||||||
when { anyOf {
|
when { anyOf {
|
||||||
branch 'test'
|
branch 'test'
|
||||||
// branch 'master'
|
branch 'master'
|
||||||
}}
|
}}
|
||||||
environment {
|
environment {
|
||||||
CREDENTIALS = credentials('docker-registry')
|
CREDENTIALS = credentials('docker-registry')
|
||||||
|
@ -73,7 +73,7 @@ pipeline {
|
||||||
stage('Deploy') {
|
stage('Deploy') {
|
||||||
when { anyOf {
|
when { anyOf {
|
||||||
branch 'test'
|
branch 'test'
|
||||||
// branch 'master'
|
branch 'master'
|
||||||
}}
|
}}
|
||||||
environment {
|
environment {
|
||||||
DOCKER_HOST = "${env.SWARM_HOST}"
|
DOCKER_HOST = "${env.SWARM_HOST}"
|
||||||
|
|
|
@ -8,7 +8,7 @@ services:
|
||||||
ports:
|
ports:
|
||||||
- 4000
|
- 4000
|
||||||
deploy:
|
deploy:
|
||||||
replicas: 2
|
replicas: ${FRONT_REPLICAS:?}
|
||||||
placement:
|
placement:
|
||||||
constraints:
|
constraints:
|
||||||
- node.role == worker
|
- node.role == worker
|
File diff suppressed because it is too large
Load Diff
|
@ -21,7 +21,7 @@
|
||||||
"@quasar/extras": "^1.14.0",
|
"@quasar/extras": "^1.14.0",
|
||||||
"axios": "^0.21.1",
|
"axios": "^0.21.1",
|
||||||
"core-js": "^3.6.5",
|
"core-js": "^3.6.5",
|
||||||
"quasar": "^2.7.1",
|
"quasar": "^2.7.3",
|
||||||
"vue": "^3.0.0",
|
"vue": "^3.0.0",
|
||||||
"vue-i18n": "^9.0.0",
|
"vue-i18n": "^9.0.0",
|
||||||
"vue-router": "^4.0.0"
|
"vue-router": "^4.0.0"
|
||||||
|
|
54
src/App.vue
54
src/App.vue
|
@ -1,6 +1,15 @@
|
||||||
<script setup>
|
<script setup>
|
||||||
|
import axios from 'axios';
|
||||||
import { useQuasar } from 'quasar';
|
import { useQuasar } from 'quasar';
|
||||||
|
import { useI18n } from 'vue-i18n';
|
||||||
|
import { useRouter } from 'vue-router';
|
||||||
|
import { useSession } from 'src/composables/useSession';
|
||||||
|
|
||||||
const quasar = useQuasar();
|
const quasar = useQuasar();
|
||||||
|
const router = useRouter();
|
||||||
|
const session = useSession();
|
||||||
|
const { t } = useI18n();
|
||||||
|
const { isLoggedIn } = session;
|
||||||
|
|
||||||
quasar.iconMapFn = (iconName) => {
|
quasar.iconMapFn = (iconName) => {
|
||||||
if (iconName.startsWith('vn:')) {
|
if (iconName.startsWith('vn:')) {
|
||||||
|
@ -11,6 +20,51 @@ quasar.iconMapFn = (iconName) => {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
function responseError(error) {
|
||||||
|
let message = error.message;
|
||||||
|
let logOut = false;
|
||||||
|
|
||||||
|
switch (error.response?.status) {
|
||||||
|
case 401:
|
||||||
|
message = 'login.loginError';
|
||||||
|
|
||||||
|
if (isLoggedIn()) {
|
||||||
|
message = 'errors.statusUnauthorized';
|
||||||
|
logOut = true;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 403:
|
||||||
|
message = 'errors.statusUnauthorized';
|
||||||
|
break;
|
||||||
|
case 500:
|
||||||
|
message = 'errors.statusInternalServerError';
|
||||||
|
break;
|
||||||
|
case 502:
|
||||||
|
message = 'errors.statusBadGateway';
|
||||||
|
break;
|
||||||
|
case 504:
|
||||||
|
message = 'errors.statusGatewayTimeout';
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
let translatedMessage = t(message);
|
||||||
|
if (!translatedMessage) translatedMessage = message;
|
||||||
|
|
||||||
|
quasar.notify({
|
||||||
|
message: translatedMessage,
|
||||||
|
type: 'negative',
|
||||||
|
});
|
||||||
|
|
||||||
|
if (logOut) {
|
||||||
|
session.destroy();
|
||||||
|
router.push({ path: '/login' });
|
||||||
|
}
|
||||||
|
|
||||||
|
return Promise.resolve(error);
|
||||||
|
}
|
||||||
|
|
||||||
|
axios.interceptors.response.use((response) => response, responseError);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
|
|
@ -0,0 +1,87 @@
|
||||||
|
import { jest, describe, expect, it, beforeAll } from '@jest/globals';
|
||||||
|
import { createWrapper } from 'app/tests/jest/jestHelpers';
|
||||||
|
import App from '../App.vue';
|
||||||
|
import { useSession } from 'src/composables/useSession';
|
||||||
|
|
||||||
|
const mockPush = jest.fn();
|
||||||
|
const mockLoggedIn = jest.fn();
|
||||||
|
const mockDestroy = jest.fn();
|
||||||
|
const session = useSession();
|
||||||
|
|
||||||
|
jest.mock('vue-router', () => ({
|
||||||
|
useRouter: () => ({
|
||||||
|
push: mockPush,
|
||||||
|
currentRoute: { value: 'myCurrentRoute' }
|
||||||
|
}),
|
||||||
|
}));
|
||||||
|
|
||||||
|
jest.mock('src/composables/useSession', () => ({
|
||||||
|
useSession: () => ({
|
||||||
|
isLoggedIn: mockLoggedIn,
|
||||||
|
destroy: mockDestroy
|
||||||
|
}),
|
||||||
|
}));
|
||||||
|
|
||||||
|
jest.mock('vue-i18n', () => ({
|
||||||
|
createI18n: () => { },
|
||||||
|
useI18n: () => ({
|
||||||
|
t: () => { }
|
||||||
|
}),
|
||||||
|
}));
|
||||||
|
|
||||||
|
describe('App', () => {
|
||||||
|
let vm;
|
||||||
|
beforeAll(() => {
|
||||||
|
const options = {
|
||||||
|
global: {
|
||||||
|
stubs: ['router-view']
|
||||||
|
}
|
||||||
|
};
|
||||||
|
vm = createWrapper(App, options).vm;
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return a login error message', async () => {
|
||||||
|
jest.spyOn(vm.quasar, 'notify');
|
||||||
|
|
||||||
|
session.isLoggedIn.mockReturnValue(false);
|
||||||
|
|
||||||
|
const response = {
|
||||||
|
response: {
|
||||||
|
status: 401
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
await vm.responseError(response);
|
||||||
|
|
||||||
|
expect(vm.quasar.notify).toHaveBeenCalledWith(expect.objectContaining(
|
||||||
|
{
|
||||||
|
type: 'negative',
|
||||||
|
message: 'login.loginError'
|
||||||
|
}
|
||||||
|
));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return an unauthorized error message', async () => {
|
||||||
|
jest.spyOn(vm.quasar, 'notify');
|
||||||
|
|
||||||
|
session.isLoggedIn.mockReturnValue(true);
|
||||||
|
|
||||||
|
const response = {
|
||||||
|
response: {
|
||||||
|
status: 401
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
await vm.responseError(response);
|
||||||
|
|
||||||
|
expect(vm.quasar.notify).toHaveBeenCalledWith(expect.objectContaining(
|
||||||
|
{
|
||||||
|
type: 'negative',
|
||||||
|
message: 'errors.statusUnauthorized'
|
||||||
|
}
|
||||||
|
));
|
||||||
|
|
||||||
|
expect(session.destroy).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
|
@ -1,17 +1,10 @@
|
||||||
import { boot } from 'quasar/wrappers';
|
|
||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
import { useSession } from 'src/composables/useSession';
|
import { useSession } from 'src/composables/useSession';
|
||||||
|
|
||||||
|
|
||||||
// Be careful when using SSR for cross-request state pollution
|
|
||||||
// due to creating a Singleton instance here;
|
|
||||||
// If any client changes this (global) instance, it might be a
|
|
||||||
// good idea to move this instance creation inside of the
|
|
||||||
// "export default () => {}" function below (which runs individually
|
|
||||||
// for each client)
|
|
||||||
const api = axios.create({ baseURL: 'https://api.example.com' });
|
|
||||||
const { getToken } = useSession();
|
const { getToken } = useSession();
|
||||||
|
|
||||||
|
axios.defaults.baseURL = '/api/';
|
||||||
|
|
||||||
axios.interceptors.request.use(
|
axios.interceptors.request.use(
|
||||||
function (context) {
|
function (context) {
|
||||||
const token = getToken();
|
const token = getToken();
|
||||||
|
@ -26,17 +19,3 @@ axios.interceptors.request.use(
|
||||||
return Promise.reject(error);
|
return Promise.reject(error);
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
export default boot(({ app }) => {
|
|
||||||
// for use inside Vue files (Options API) through this.$axios and this.$api
|
|
||||||
|
|
||||||
app.config.globalProperties.$axios = axios;
|
|
||||||
// ^ ^ ^ this will allow you to use this.$axios (for Vue Options API form)
|
|
||||||
// so you won't necessarily have to import axios in each vue file
|
|
||||||
|
|
||||||
app.config.globalProperties.$api = api;
|
|
||||||
// ^ ^ ^ this will allow you to use this.$api (for Vue Options API form)
|
|
||||||
// so you can easily perform requests against your app's API
|
|
||||||
});
|
|
||||||
|
|
||||||
export { api };
|
|
||||||
|
|
|
@ -60,6 +60,11 @@ async function fetch() {
|
||||||
params: { filter },
|
params: { filter },
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (!data) {
|
||||||
|
isLoading.value = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
hasMoreData.value = data.length === rowsPerPage;
|
hasMoreData.value = data.length === rowsPerPage;
|
||||||
|
|
||||||
for (const row of data) rows.value.push(row);
|
for (const row of data) rows.value.push(row);
|
||||||
|
|
|
@ -39,7 +39,7 @@ function updatePreferences() {
|
||||||
}
|
}
|
||||||
|
|
||||||
async function saveDarkMode(value) {
|
async function saveDarkMode(value) {
|
||||||
const query = `/api/UserConfigs/${user.value.id}`;
|
const query = `/UserConfigs/${user.value.id}`;
|
||||||
await axios.patch(query, {
|
await axios.patch(query, {
|
||||||
darkMode: value,
|
darkMode: value,
|
||||||
});
|
});
|
||||||
|
@ -47,7 +47,7 @@ async function saveDarkMode(value) {
|
||||||
}
|
}
|
||||||
|
|
||||||
async function saveLanguage(value) {
|
async function saveLanguage(value) {
|
||||||
const query = `/api/Accounts/${user.value.id}`;
|
const query = `/Accounts/${user.value.id}`;
|
||||||
await axios.patch(query, {
|
await axios.patch(query, {
|
||||||
lang: value,
|
lang: value,
|
||||||
});
|
});
|
||||||
|
|
|
@ -57,7 +57,7 @@ export function useNavigation() {
|
||||||
};
|
};
|
||||||
|
|
||||||
async function fetchFavorites() {
|
async function fetchFavorites() {
|
||||||
const response = await axios.get('api/starredModules/getStarredModules');
|
const response = await axios.get('StarredModules/getStarredModules');
|
||||||
|
|
||||||
const filteredModules = modules.value.filter((module) => {
|
const filteredModules = modules.value.filter((module) => {
|
||||||
return response.data.find((element) => element.moduleFk == salixModules[module.name]);
|
return response.data.find((element) => element.moduleFk == salixModules[module.name]);
|
||||||
|
@ -72,7 +72,7 @@ export function useNavigation() {
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
|
|
||||||
const params = { moduleName: salixModules[moduleName] };
|
const params = { moduleName: salixModules[moduleName] };
|
||||||
const query = 'api/starredModules/toggleStarredModule';
|
const query = 'StarredModules/toggleStarredModule';
|
||||||
await axios.post(query, params);
|
await axios.post(query, params);
|
||||||
|
|
||||||
updateFavorites(moduleName);
|
updateFavorites(moduleName);
|
||||||
|
|
|
@ -5,7 +5,7 @@ export function useRole() {
|
||||||
const state = useState();
|
const state = useState();
|
||||||
|
|
||||||
async function fetch() {
|
async function fetch() {
|
||||||
const { data } = await axios.get('/api/accounts/acl');
|
const { data } = await axios.get('Accounts/acl');
|
||||||
const roles = data.roles.map(userRoles => userRoles.role.name);
|
const roles = data.roles.map(userRoles => userRoles.role.name);
|
||||||
|
|
||||||
const userData = {
|
const userData = {
|
||||||
|
|
|
@ -19,6 +19,8 @@ export default {
|
||||||
errors: {
|
errors: {
|
||||||
statusUnauthorized: 'Access denied',
|
statusUnauthorized: 'Access denied',
|
||||||
statusInternalServerError: 'An internal server error has ocurred',
|
statusInternalServerError: 'An internal server error has ocurred',
|
||||||
|
statusBadGateway: 'It seems that the server has fall down',
|
||||||
|
statusGatewayTimeout: 'Could not contact the server',
|
||||||
},
|
},
|
||||||
login: {
|
login: {
|
||||||
title: 'Login',
|
title: 'Login',
|
||||||
|
@ -54,6 +56,16 @@ export default {
|
||||||
list: 'List',
|
list: 'List',
|
||||||
createTicket: 'Create ticket',
|
createTicket: 'Create ticket',
|
||||||
basicData: 'Basic Data'
|
basicData: 'Basic Data'
|
||||||
|
},
|
||||||
|
boxing: {
|
||||||
|
expedition: 'Expedition',
|
||||||
|
item: 'Item',
|
||||||
|
created: 'Created',
|
||||||
|
worker: 'Worker',
|
||||||
|
selectTime: 'Select time:',
|
||||||
|
selectVideo: 'Select video:',
|
||||||
|
notFound: 'No videos available'
|
||||||
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
components: {
|
components: {
|
||||||
|
|
|
@ -19,6 +19,8 @@ export default {
|
||||||
errors: {
|
errors: {
|
||||||
statusUnauthorized: 'Acceso denegado',
|
statusUnauthorized: 'Acceso denegado',
|
||||||
statusInternalServerError: 'Ha ocurrido un error interno del servidor',
|
statusInternalServerError: 'Ha ocurrido un error interno del servidor',
|
||||||
|
statusBadGateway: 'Parece ser que el servidor ha caído',
|
||||||
|
statusGatewayTimeout: 'No se ha podido contactar con el servidor',
|
||||||
},
|
},
|
||||||
login: {
|
login: {
|
||||||
title: 'Inicio de sesión',
|
title: 'Inicio de sesión',
|
||||||
|
@ -54,6 +56,15 @@ export default {
|
||||||
list: 'Listado',
|
list: 'Listado',
|
||||||
createTicket: 'Crear ticket',
|
createTicket: 'Crear ticket',
|
||||||
basicData: 'Datos básicos'
|
basicData: 'Datos básicos'
|
||||||
|
},
|
||||||
|
boxing: {
|
||||||
|
expedition: 'Expedición',
|
||||||
|
item: 'Artículo',
|
||||||
|
created: 'Creado',
|
||||||
|
worker: 'Trabajador',
|
||||||
|
selectTime: 'Seleccionar hora:',
|
||||||
|
selectVideo: 'Seleccionar vídeo:',
|
||||||
|
notFound: 'No hay vídeos disponibles'
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
components: {
|
components: {
|
||||||
|
|
|
@ -17,7 +17,7 @@ const entityId = computed(function () {
|
||||||
const customer = ref({});
|
const customer = ref({});
|
||||||
|
|
||||||
async function fetch() {
|
async function fetch() {
|
||||||
const { data } = await axios.get(`/api/Clients/${entityId.value}`);
|
const { data } = await axios.get(`Clients/${entityId.value}`);
|
||||||
|
|
||||||
if (data) customer.value = data;
|
if (data) customer.value = data;
|
||||||
}
|
}
|
||||||
|
|
|
@ -13,7 +13,7 @@ function navigate(id) {
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<q-page class="q-pa-md">
|
<q-page class="q-pa-md">
|
||||||
<smart-card url="/api/Clients" sort-by="id DESC" @on-navigate="navigate" auto-load>
|
<smart-card url="/Clients" sort-by="id DESC" @on-navigate="navigate" auto-load>
|
||||||
<template #labels="{ row }">
|
<template #labels="{ row }">
|
||||||
<q-list>
|
<q-list>
|
||||||
<q-item class="q-pa-none">
|
<q-item class="q-pa-none">
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref } from 'vue';
|
import { ref } from 'vue';
|
||||||
import { useState } from 'src/composables/useState';
|
import { useState } from 'src/composables/useState';
|
||||||
import LeftMenu from 'src/components/LeftMenu.vue';
|
//import LeftMenu from 'src/components/LeftMenu.vue';
|
||||||
|
|
||||||
const state = useState();
|
const state = useState();
|
||||||
const miniState = ref(true);
|
const miniState = ref(true);
|
||||||
|
|
|
@ -17,40 +17,25 @@ const password = ref('');
|
||||||
const keepLogin = ref(true);
|
const keepLogin = ref(true);
|
||||||
|
|
||||||
async function onSubmit() {
|
async function onSubmit() {
|
||||||
try {
|
const { data } = await axios.post('Accounts/login', {
|
||||||
const { data } = await axios.post('/api/accounts/login', {
|
user: username.value,
|
||||||
user: username.value,
|
password: password.value,
|
||||||
password: password.value,
|
});
|
||||||
});
|
|
||||||
|
|
||||||
await session.login(data.token, keepLogin.value);
|
if (!data) return;
|
||||||
|
|
||||||
quasar.notify({
|
await session.login(data.token, keepLogin.value);
|
||||||
message: t('login.loginSuccess'),
|
|
||||||
type: 'positive',
|
|
||||||
});
|
|
||||||
|
|
||||||
const currentRoute = router.currentRoute.value;
|
quasar.notify({
|
||||||
if (currentRoute.query && currentRoute.query.redirect) {
|
message: t('login.loginSuccess'),
|
||||||
router.push(currentRoute.query.redirect);
|
type: 'positive',
|
||||||
} else {
|
});
|
||||||
router.push({ name: 'Dashboard' });
|
|
||||||
}
|
const currentRoute = router.currentRoute.value;
|
||||||
} catch (error) {
|
if (currentRoute.query && currentRoute.query.redirect) {
|
||||||
if (axios.isAxiosError(error)) {
|
router.push(currentRoute.query.redirect);
|
||||||
const errorCode = error.response && error.response.status;
|
} else {
|
||||||
if (errorCode === 401) {
|
router.push({ name: 'Dashboard' });
|
||||||
quasar.notify({
|
|
||||||
message: t('login.loginError'),
|
|
||||||
type: 'negative',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
quasar.notify({
|
|
||||||
message: t('errors.statusInternalServerError'),
|
|
||||||
type: 'negative',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -17,6 +17,10 @@ describe('Login', () => {
|
||||||
vm = createWrapper(Login).vm;
|
vm = createWrapper(Login).vm;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
jest.clearAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
it('should successfully set the token into session', async () => {
|
it('should successfully set the token into session', async () => {
|
||||||
const expectedUser = {
|
const expectedUser = {
|
||||||
id: 999,
|
id: 999,
|
||||||
|
@ -29,7 +33,7 @@ describe('Login', () => {
|
||||||
}
|
}
|
||||||
jest.spyOn(axios, 'post').mockResolvedValue({ data: { token: 'token' } });
|
jest.spyOn(axios, 'post').mockResolvedValue({ data: { token: 'token' } });
|
||||||
jest.spyOn(axios, 'get').mockResolvedValue({ data: { roles: [], user: expectedUser } });
|
jest.spyOn(axios, 'get').mockResolvedValue({ data: { roles: [], user: expectedUser } });
|
||||||
jest.spyOn(vm.quasar, 'notify')
|
jest.spyOn(vm.quasar, 'notify');
|
||||||
|
|
||||||
expect(vm.session.getToken()).toEqual('');
|
expect(vm.session.getToken()).toEqual('');
|
||||||
|
|
||||||
|
@ -43,15 +47,11 @@ describe('Login', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should not set the token into session if any error occurred', async () => {
|
it('should not set the token into session if any error occurred', async () => {
|
||||||
jest.spyOn(axios, 'post').mockRejectedValue(new Error('error'));
|
jest.spyOn(axios, 'post').mockReturnValue({ data: null });
|
||||||
jest.spyOn(vm.quasar, 'notify')
|
jest.spyOn(vm.quasar, 'notify');
|
||||||
|
|
||||||
expect(vm.session.getToken()).toEqual('');
|
|
||||||
|
|
||||||
await vm.onSubmit();
|
await vm.onSubmit();
|
||||||
|
|
||||||
expect(vm.quasar.notify).toHaveBeenCalledWith(expect.objectContaining(
|
expect(vm.quasar.notify).not.toHaveBeenCalled();
|
||||||
{ 'type': 'negative' }
|
|
||||||
));
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -0,0 +1,159 @@
|
||||||
|
<script setup>
|
||||||
|
import { useI18n } from 'vue-i18n';
|
||||||
|
import { computed, ref, onMounted } from 'vue';
|
||||||
|
import { useRouter } from 'vue-router';
|
||||||
|
import axios from 'axios';
|
||||||
|
import { date, useQuasar } from 'quasar';
|
||||||
|
|
||||||
|
const router = useRouter();
|
||||||
|
const { t } = useI18n();
|
||||||
|
const quasar = useQuasar();
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
await fetch();
|
||||||
|
});
|
||||||
|
|
||||||
|
const entityId = computed(function () {
|
||||||
|
return router.currentRoute.value.params.id;
|
||||||
|
});
|
||||||
|
|
||||||
|
const expeditions = ref({});
|
||||||
|
const lastExpedition = ref();
|
||||||
|
const slide = ref(null);
|
||||||
|
const videoList = ref([]);
|
||||||
|
const time = ref({
|
||||||
|
min: 0,
|
||||||
|
max: 24,
|
||||||
|
});
|
||||||
|
|
||||||
|
async function fetch() {
|
||||||
|
const filter = {
|
||||||
|
where: {
|
||||||
|
ticketFk: entityId.value,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const { data } = await axios.get(`/Expeditions/filter`, {
|
||||||
|
params: { filter },
|
||||||
|
});
|
||||||
|
|
||||||
|
if (data) expeditions.value = data;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getVideoList(expeditionId, timed) {
|
||||||
|
lastExpedition.value = expeditionId;
|
||||||
|
const params = {
|
||||||
|
id: expeditionId,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (timed) {
|
||||||
|
Object.assign(params, { from: timed.min, to: timed.max });
|
||||||
|
}
|
||||||
|
const { data } = await axios.get(`/Boxings/getVideoList`, { params: params });
|
||||||
|
|
||||||
|
const list = [];
|
||||||
|
for (const video of data) {
|
||||||
|
const videName = video.split('.')[0].split('T')[1].replaceAll('-', ':');
|
||||||
|
list.push({
|
||||||
|
label: videName,
|
||||||
|
value: video,
|
||||||
|
url: `api/Boxings/getVideo?id=${expeditionId}&filename=${video}`,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
videoList.value = list.reverse();
|
||||||
|
if (list[0]) {
|
||||||
|
slide.value = list[0].value;
|
||||||
|
time.value = {
|
||||||
|
min: parseInt(list[0].label.split(':')[0]),
|
||||||
|
max: parseInt(list[list.length - 1].label.split(':')[0]),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!data.length) {
|
||||||
|
return quasar.notify({
|
||||||
|
message: t('ticket.boxing.notFound'),
|
||||||
|
type: 'negative',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<q-layout view="hhh lpr ffr" class="fit">
|
||||||
|
<q-drawer show-if-above side="right" bordered>
|
||||||
|
<q-scroll-area class="fit">
|
||||||
|
<q-list bordered separator style="max-width: 318px">
|
||||||
|
<q-item v-if="lastExpedition && videoList.length">
|
||||||
|
<q-item-section>
|
||||||
|
<q-item-label class="text-h6">
|
||||||
|
{{ t('ticket.boxing.selectTime') }} ({{ time.min }}-{{ time.max }})
|
||||||
|
</q-item-label>
|
||||||
|
<q-range
|
||||||
|
v-model="time"
|
||||||
|
@change="getVideoList(lastExpedition, time)"
|
||||||
|
:min="0"
|
||||||
|
:max="24"
|
||||||
|
:step="1"
|
||||||
|
:left-label-value="time.min + ':00'"
|
||||||
|
:right-label-value="time.max + ':00'"
|
||||||
|
label
|
||||||
|
markers
|
||||||
|
snap
|
||||||
|
color="orange"
|
||||||
|
/>
|
||||||
|
</q-item-section>
|
||||||
|
</q-item>
|
||||||
|
<q-item v-if="lastExpedition && videoList.length">
|
||||||
|
<q-item-section>
|
||||||
|
<q-select
|
||||||
|
color="orange"
|
||||||
|
v-model="slide"
|
||||||
|
:options="videoList"
|
||||||
|
:label="t('ticket.boxing.selectVideo')"
|
||||||
|
emit-value
|
||||||
|
map-options
|
||||||
|
>
|
||||||
|
<template #prepend>
|
||||||
|
<q-icon name="schedule" />
|
||||||
|
</template>
|
||||||
|
</q-select>
|
||||||
|
</q-item-section>
|
||||||
|
</q-item>
|
||||||
|
<q-item
|
||||||
|
v-for="expedition in expeditions"
|
||||||
|
:key="expedition.id"
|
||||||
|
@click="getVideoList(expedition.id)"
|
||||||
|
clickable
|
||||||
|
v-ripple
|
||||||
|
>
|
||||||
|
<q-item-section>
|
||||||
|
<q-item-label class="text-h6">#{{ expedition.id }}</q-item-label>
|
||||||
|
</q-item-section>
|
||||||
|
<q-item-section>
|
||||||
|
<q-item-label caption>{{ t('ticket.boxing.created') }}</q-item-label>
|
||||||
|
<q-item-label>
|
||||||
|
{{ date.formatDate(expedition.created, 'YYYY-MM-DD HH:mm:ss') }}
|
||||||
|
</q-item-label>
|
||||||
|
<q-item-label caption>{{ t('ticket.boxing.item') }}</q-item-label>
|
||||||
|
<q-item-label>{{ expedition.packagingItemFk }}</q-item-label>
|
||||||
|
<q-item-label caption>{{ t('ticket.boxing.worker') }}</q-item-label>
|
||||||
|
<q-item-label>{{ expedition.userName }}</q-item-label>
|
||||||
|
</q-item-section>
|
||||||
|
</q-item>
|
||||||
|
</q-list>
|
||||||
|
</q-scroll-area>
|
||||||
|
</q-drawer>
|
||||||
|
|
||||||
|
<q-page-container>
|
||||||
|
<q-page>
|
||||||
|
<q-card>
|
||||||
|
<q-carousel animated v-model="slide" height="max-content">
|
||||||
|
<q-carousel-slide v-for="video in videoList" :key="video.value" :name="video.value">
|
||||||
|
<q-video :src="video.url" :ratio="16 / 9" />
|
||||||
|
</q-carousel-slide>
|
||||||
|
</q-carousel>
|
||||||
|
</q-card>
|
||||||
|
</q-page>
|
||||||
|
</q-page-container>
|
||||||
|
</q-layout>
|
||||||
|
</template>
|
|
@ -1,23 +1,23 @@
|
||||||
<script setup>
|
<script setup>
|
||||||
import { computed } from 'vue';
|
//import { computed } from 'vue';
|
||||||
import { useState } from 'src/composables/useState';
|
//import { useState } from 'src/composables/useState';
|
||||||
import { useRouter } from 'vue-router';
|
//import { useRouter } from 'vue-router';
|
||||||
|
|
||||||
const state = useState();
|
//const state = useState();
|
||||||
const router = useRouter();
|
//const router = useRouter();
|
||||||
const entityId = computed(function () {
|
/*const entityId = computed(function () {
|
||||||
return router.currentRoute.value.params.id;
|
return router.currentRoute.value.params.id;
|
||||||
});
|
});*/
|
||||||
</script>
|
</script>
|
||||||
<template>
|
<template>
|
||||||
<q-drawer v-model="state.drawer.value" show-if-above :width="200" :breakpoint="500">
|
<!--<q-drawer v-model="state.drawer.value" show-if-above :width="200" :breakpoint="500">
|
||||||
<q-scroll-area class="fit text-grey-8">
|
<q-scroll-area class="fit text-grey-8">
|
||||||
<router-link :to="{ path: '/customer/list' }">
|
<router-link :to="{ path: '/customer/list' }">
|
||||||
<q-icon name="arrow_back" size="md" color="primary" />
|
<q-icon name="arrow_back" size="md" color="primary" />
|
||||||
</router-link>
|
</router-link>
|
||||||
<div>Customer ID: {{ entityId }}</div>
|
<div>Customer ID: {{ entityId }}</div>
|
||||||
</q-scroll-area>
|
</q-scroll-area>
|
||||||
</q-drawer>
|
</q-drawer>-->
|
||||||
<q-page-container>
|
<q-page-container>
|
||||||
<router-view></router-view>
|
<router-view></router-view>
|
||||||
</q-page-container>
|
</q-page-container>
|
||||||
|
|
|
@ -0,0 +1,69 @@
|
||||||
|
import { jest, describe, expect, it, beforeAll } from '@jest/globals';
|
||||||
|
import { createWrapper, axios } from 'app/tests/jest/jestHelpers';
|
||||||
|
import TicketBoxing from '../TicketBoxing.vue';
|
||||||
|
|
||||||
|
const mockPush = jest.fn();
|
||||||
|
|
||||||
|
jest.mock('vue-router', () => ({
|
||||||
|
useRouter: () => ({
|
||||||
|
push: mockPush,
|
||||||
|
currentRoute: {
|
||||||
|
value: {
|
||||||
|
params: {
|
||||||
|
id: 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
}));
|
||||||
|
|
||||||
|
describe('TicketBoxing', () => {
|
||||||
|
let vm;
|
||||||
|
beforeAll(() => {
|
||||||
|
vm = createWrapper(TicketBoxing).vm;
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
jest.clearAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('getVideoList()', () => {
|
||||||
|
it('should when response videoList use to list', async () => {
|
||||||
|
const expeditionId = 1;
|
||||||
|
const timed = {
|
||||||
|
min: 1,
|
||||||
|
max: 2
|
||||||
|
}
|
||||||
|
const videoList = [
|
||||||
|
"2022-01-01T01-01-00.mp4",
|
||||||
|
"2022-02-02T02-02-00.mp4",
|
||||||
|
"2022-03-03T03-03-00.mp4",
|
||||||
|
]
|
||||||
|
|
||||||
|
jest.spyOn(axios, 'get').mockResolvedValue({ data: videoList });
|
||||||
|
jest.spyOn(vm.quasar, 'notify');
|
||||||
|
|
||||||
|
await vm.getVideoList(expeditionId, timed);
|
||||||
|
|
||||||
|
expect(vm.videoList.length).toEqual(videoList.length);
|
||||||
|
expect(vm.slide).toEqual(videoList.reverse()[0]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should if not have video show notify', async () => {
|
||||||
|
const expeditionId = 1;
|
||||||
|
const timed = {
|
||||||
|
min: 1,
|
||||||
|
max: 2
|
||||||
|
}
|
||||||
|
|
||||||
|
jest.spyOn(axios, 'get').mockResolvedValue({ data: [] });
|
||||||
|
jest.spyOn(vm.quasar, 'notify')
|
||||||
|
|
||||||
|
await vm.getVideoList(expeditionId, timed);
|
||||||
|
|
||||||
|
expect(vm.quasar.notify).toHaveBeenCalledWith(expect.objectContaining(
|
||||||
|
{ 'type': 'negative' }
|
||||||
|
));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
|
@ -1,7 +1,7 @@
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref } from 'vue';
|
import { ref } from 'vue';
|
||||||
import { useState } from 'src/composables/useState';
|
import { useState } from 'src/composables/useState';
|
||||||
import LeftMenu from 'src/components/LeftMenu.vue';
|
//import LeftMenu from 'src/components/LeftMenu.vue';
|
||||||
|
|
||||||
const state = useState();
|
const state = useState();
|
||||||
const miniState = ref(true);
|
const miniState = ref(true);
|
||||||
|
@ -18,9 +18,9 @@ const miniState = ref(true);
|
||||||
:width="256"
|
:width="256"
|
||||||
:breakpoint="500"
|
:breakpoint="500"
|
||||||
>
|
>
|
||||||
<q-scroll-area class="fit text-grey-8">
|
<!--<q-scroll-area class="fit text-grey-8">
|
||||||
<LeftMenu />
|
<LeftMenu />
|
||||||
</q-scroll-area>
|
</q-scroll-area>-->
|
||||||
</q-drawer>
|
</q-drawer>
|
||||||
<q-page-container>
|
<q-page-container>
|
||||||
<router-view></router-view>
|
<router-view></router-view>
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { route } from 'quasar/wrappers';
|
import { route } from 'quasar/wrappers';
|
||||||
import { createRouter, createMemoryHistory, createWebHistory, createWebHashHistory } from 'vue-router';
|
import { createRouter, createMemoryHistory, createWebHistory, createWebHashHistory } from 'vue-router';
|
||||||
import { Notify } from 'quasar';
|
// import { Notify } from 'quasar';
|
||||||
import routes from './routes';
|
import routes from './routes';
|
||||||
import { i18n } from 'src/boot/i18n';
|
import { i18n } from 'src/boot/i18n';
|
||||||
import { useState } from 'src/composables/useState';
|
import { useState } from 'src/composables/useState';
|
||||||
|
@ -46,20 +46,20 @@ export default route(function (/* { store, ssrContext } */) {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isLoggedIn()) {
|
if (isLoggedIn()) {
|
||||||
try {
|
// try {
|
||||||
const stateRoles = state.getRoles().value;
|
const stateRoles = state.getRoles().value;
|
||||||
if (stateRoles.length === 0) {
|
if (stateRoles.length === 0) {
|
||||||
await role.fetch();
|
await role.fetch();
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
Notify.create({
|
|
||||||
message: t('errors.statusUnauthorized'),
|
|
||||||
type: 'negative',
|
|
||||||
});
|
|
||||||
|
|
||||||
session.destroy();
|
|
||||||
return next({ path: '/login' });
|
|
||||||
}
|
}
|
||||||
|
// } catch (error) {
|
||||||
|
// Notify.create({
|
||||||
|
// message: t('errors.statusUnauthorized'),
|
||||||
|
// type: 'negative',
|
||||||
|
// });
|
||||||
|
|
||||||
|
// session.destroy();
|
||||||
|
// return next({ path: '/login' });
|
||||||
|
// }
|
||||||
|
|
||||||
const matches = to.matched;
|
const matches = to.matched;
|
||||||
const hasRequiredRoles = matches.every(route => {
|
const hasRequiredRoles = matches.every(route => {
|
||||||
|
|
|
@ -51,8 +51,16 @@ export default {
|
||||||
title: 'basicData'
|
title: 'basicData'
|
||||||
},
|
},
|
||||||
component: () => import('src/pages/Ticket/Card/TicketBasicData.vue'),
|
component: () => import('src/pages/Ticket/Card/TicketBasicData.vue'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'boxing',
|
||||||
|
name: 'TicketBoxing',
|
||||||
|
meta: {
|
||||||
|
title: 'boxing'
|
||||||
|
},
|
||||||
|
component: () => import('src/pages/Ticket/Card/TicketBoxing.vue'),
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
};
|
};
|
||||||
|
|
|
@ -0,0 +1,38 @@
|
||||||
|
describe('TicketBoxing', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
const ticketId = 1;
|
||||||
|
cy.viewport(1280, 720)
|
||||||
|
cy.login('developer')
|
||||||
|
cy.visit(`/#/ticket/${ticketId}/boxing`);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should load expeditions of ticket', () => {
|
||||||
|
cy.get('div[class="q-item__label text-h6"]').eq(0).should('have.text', '#1');
|
||||||
|
cy.get('div[class="q-item__label text-h6"]').eq(1).should('have.text', '#2');
|
||||||
|
cy.get('div[class="q-item__label text-h6"]').eq(2).should('have.text', '#3');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should show error if not have video list', () => {
|
||||||
|
cy.get('div[class="q-item__label text-h6"]').eq(0).click();
|
||||||
|
cy.get('.q-notification__message').should('have.text', 'No videos available');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should show select time and video if have video list', () => {
|
||||||
|
cy.intercept(
|
||||||
|
{
|
||||||
|
method: 'GET',
|
||||||
|
url: '/api/Boxings/*',
|
||||||
|
},
|
||||||
|
[
|
||||||
|
"2022-01-01T01-01-00.mp4",
|
||||||
|
"2022-02-02T02-02-00.mp4",
|
||||||
|
"2022-03-03T03-03-00.mp4",
|
||||||
|
]
|
||||||
|
).as('getVideoList');
|
||||||
|
cy.get('.q-list > :nth-child(3)').click();
|
||||||
|
|
||||||
|
cy.get('.q-list > :nth-child(1)').should('be.visible');
|
||||||
|
cy.get('.q-list > :nth-child(2)').should('be.visible');
|
||||||
|
|
||||||
|
});
|
||||||
|
});
|
Loading…
Reference in New Issue