feat: refs #8534 implement navigation and state query guards for improved routing control #1621

Merged
alexm merged 14 commits from 8534-wait_queries into dev 2025-03-28 08:32:18 +00:00
5 changed files with 140 additions and 90 deletions

View File

@ -0,0 +1,36 @@
import { describe, it, expect, vi } from 'vitest';
import { ref, nextTick } from 'vue';
import { stateQueryGuard } from 'src/router/hooks';
import { useStateQueryStore } from 'src/stores/useStateQueryStore';

esto es nuevo?

esto es nuevo?
vi.mock('src/stores/useStateQueryStore', () => {
const isLoading = ref(true);
return {
useStateQueryStore: () => ({
isLoading: () => isLoading,
setLoading: isLoading,
}),
};
});
describe('hooks', () => {
describe('stateQueryGuard', () => {
const foo = { name: 'foo' };
it('should wait until the state query is not loading and then call next()', async () => {
const next = vi.fn();
stateQueryGuard(foo, { name: 'bar' }, next);
expect(next).not.toHaveBeenCalled();
useStateQueryStore().setLoading.value = false;
await nextTick();
expect(next).toHaveBeenCalled();
});
it('should ignore if both routes are the same', () => {
const next = vi.fn();
stateQueryGuard(foo, foo, next);
expect(next).toHaveBeenCalled();
});
});
});

95
src/router/hooks.js Normal file
View File

@ -0,0 +1,95 @@
import { useRole } from 'src/composables/useRole';
Review

He separado las funciones del router asi se puede testear las funciones sin pasar por el router real de vue

He separado las funciones del router asi se puede testear las funciones sin pasar por el router real de vue
import { useUserConfig } from 'src/composables/useUserConfig';
import { useTokenConfig } from 'src/composables/useTokenConfig';
import { useAcl } from 'src/composables/useAcl';
import { isLoggedIn } from 'src/utils/session';
import { useSession } from 'src/composables/useSession';
import { useStateQueryStore } from 'src/stores/useStateQueryStore';
import { watch } from 'vue';
import { i18n } from 'src/boot/i18n';
let session = null;
const { t, te } = i18n.global;
export async function navigationGuard(to, from, next, Router, state) {
if (!session) session = useSession();
const outLayout = Router.options.routes[0].children.map((r) => r.name);
if (!session.isLoggedIn() && !outLayout.includes(to.name)) {
return next({ name: 'Login', query: { redirect: to.fullPath } });
}
if (isLoggedIn()) {
const stateRoles = state.getRoles().value;
if (stateRoles.length === 0) {
await useRole().fetch();
await useAcl().fetch();
await useUserConfig().fetch();
await useTokenConfig().fetch();
}
const matches = to.matched;
const hasRequiredAcls = matches.every((route) => {
const meta = route.meta;
if (!meta?.acls) return true;
return useAcl().hasAny(meta.acls);
});
if (!hasRequiredAcls) return next({ path: '/' });
}
next();
}
export async function stateQueryGuard(to, from, next) {
Outdated
Review

Esta es la nueva

Esta es la nueva
Review

Esta es la nueva, todo lo demás está movido

Esta es la nueva, todo lo demás está movido
if (to.name !== from.name) {
const stateQuery = useStateQueryStore();
await waitUntilFalse(stateQuery.isLoading());
}
next();
}
export function setPageTitle(to) {
let title = t(`login.title`);
const matches = to.matched;
if (matches && matches.length > 1) {
const module = matches[1];
const moduleTitle = module.meta?.title;
if (moduleTitle) {
title = t(`globals.pageTitles.${moduleTitle}`);
}
}
const childPage = to.meta;
const childPageTitle = childPage?.title;
if (childPageTitle && matches.length > 2) {
if (title != '') title += ': ';
const moduleLocale = `globals.pageTitles.${childPageTitle}`;
const pageTitle = te(moduleLocale)
? t(moduleLocale)
: t(`globals.pageTitles.${childPageTitle}`);
const idParam = to.params?.id;
const idPageTitle = `${idParam} - ${pageTitle}`;
const builtTitle = idParam ? idPageTitle : pageTitle;
title += builtTitle;
}
document.title = title;
}
function waitUntilFalse(ref) {
return new Promise((resolve) => {
if (!ref.value) return resolve();
const stop = watch(
ref,
(val) => {
if (!val) {
stop();
resolve();
}
},
{ immediate: true },
);
});
}

View File

@ -6,101 +6,25 @@ import {
createWebHashHistory,
} from 'vue-router';
import routes from './routes';
import { i18n } from 'src/boot/i18n';
import { useState } from 'src/composables/useState';
import { useRole } from 'src/composables/useRole';
import { useUserConfig } from 'src/composables/useUserConfig';
import { useTokenConfig } from 'src/composables/useTokenConfig';
import { useAcl } from 'src/composables/useAcl';
import { isLoggedIn } from 'src/utils/session';
import { useSession } from 'src/composables/useSession';
import { navigationGuard, setPageTitle, stateQueryGuard } from './hooks';
let session = null;
const { t, te } = i18n.global;
const createHistory = process.env.SERVER
? createMemoryHistory
: process.env.VUE_ROUTER_MODE === 'history'
? createWebHistory
: createWebHashHistory;
const webHistory =
process.env.VUE_ROUTER_MODE === 'history' ? createWebHistory : createWebHashHistory;
const createHistory = process.env.SERVER ? createMemoryHistory : webHistory;
const Router = createRouter({
scrollBehavior: () => ({ left: 0, top: 0 }),
routes,
// Leave this as is and make changes in quasar.conf.js instead!
// quasar.conf.js -> build -> vueRouterMode
// quasar.conf.js -> build -> publicPath
history: createHistory(process.env.VUE_ROUTER_BASE),
});
/*
* If not building with SSR mode, you can
* directly export the Router instantiation;
*
* The function below can be async too; either use
* async/await or return a Promise which resolves
* with the Router instance.
*/
export { Router };
export default defineRouter(function (/* { store, ssrContext } */) {
export default defineRouter(() => {
const state = useState();
Router.beforeEach(async (to, from, next) => {
if (!session) session = useSession();
const outLayout = Router.options.routes[0].children.map((r) => r.name);
if (!session.isLoggedIn() && !outLayout.includes(to.name)) {
return next({ name: 'Login', query: { redirect: to.fullPath } });
}
if (isLoggedIn()) {
const stateRoles = state.getRoles().value;
if (stateRoles.length === 0) {
await useRole().fetch();
await useAcl().fetch();
await useUserConfig().fetch();
await useTokenConfig().fetch();
}
const matches = to.matched;
const hasRequiredAcls = matches.every((route) => {
const meta = route.meta;
if (!meta?.acls) return true;
return useAcl().hasAny(meta.acls);
});
if (!hasRequiredAcls) return next({ path: '/' });
}
next();
});
Router.afterEach((to) => {
let title = t(`login.title`);
const matches = to.matched;
if (matches && matches.length > 1) {
const module = matches[1];
const moduleTitle = module.meta && module.meta.title;
if (moduleTitle) {
title = t(`globals.pageTitles.${moduleTitle}`);
}
}
const childPage = to.meta;
const childPageTitle = childPage && childPage.title;
if (childPageTitle && matches.length > 2) {
if (title != '') title += ': ';
const moduleLocale = `globals.pageTitles.${childPageTitle}`;
const pageTitle = te(moduleLocale)
? t(moduleLocale)
: t(`globals.pageTitles.${childPageTitle}`);
const idParam = to.params && to.params.id;
const idPageTitle = `${idParam} - ${pageTitle}`;
const builtTitle = idParam ? idPageTitle : pageTitle;
title += builtTitle;
}
document.title = title;
});
Router.beforeEach((to, from, next) => navigationGuard(to, from, next, Router, state));
Review

esta es una lastima que aqui no

esta es una lastima que aqui no
Router.beforeEach(stateQueryGuard);

Router.beforeEach( stateQueryGuard);

Router.beforeEach( stateQueryGuard);
Router.afterEach(setPageTitle);

Router.afterEach(setPageTitle);

Router.afterEach(setPageTitle);
Router.onError(({ message }) => {
const errorMessages = [

View File

@ -1,6 +1,6 @@
Cypress.Commands.add('selectTravel', (warehouse = '1') => {
cy.get('i[data-cy="Travel_icon"]').click();
cy.get('input[data-cy="Warehouse Out_select"]').type(warehouse);
cy.selectOption('input[data-cy="Warehouse Out_select"]', warehouse);
cy.get('div[role="listbox"] > div > div[role="option"]').eq(0).click();
cy.get('button[data-cy="save-filter-travel-form"]').click();
cy.get('tr').eq(1).click();
@ -9,7 +9,6 @@ Cypress.Commands.add('selectTravel', (warehouse = '1') => {
Cypress.Commands.add('deleteEntry', () => {
cy.get('[data-cy="descriptor-more-opts"]').should('be.visible').click();
cy.waitForElement('div[data-cy="delete-entry"]').click();
cy.url().should('include', 'list');
});
Cypress.Commands.add('createEntry', () => {

View File

@ -28,12 +28,8 @@ describe('EntryDescriptor', () => {
cy.get('.q-notification__message')
.eq(2)
.should('have.text', 'Entry prices recalculated');
cy.get('[data-cy="descriptor-more-opts"]').click();
Outdated
Review

deleteEntry ya tiene
cy.get('[data-cy="descriptor-more-opts"]').click(); dentro

deleteEntry ya tiene `cy.get('[data-cy="descriptor-more-opts"]').click();` dentro
cy.deleteEntry();
cy.log(previousUrl);
cy.visit(previousUrl);
cy.waitForElement('[data-cy="entry-buys"]');