diff --git a/src/components/ui/VnMoreOptions.vue b/src/components/ui/VnMoreOptions.vue index 984e2b64f..bc81233d5 100644 --- a/src/components/ui/VnMoreOptions.vue +++ b/src/components/ui/VnMoreOptions.vue @@ -9,7 +9,7 @@ data-cy="descriptor-more-opts" > - {{ $t('components.cardDescriptor.moreOptions') }} + {{ $t('components.vnDescriptor.moreOptions') }} diff --git a/src/pages/Claim/ClaimList.vue b/src/pages/Claim/ClaimList.vue index 06996c2c1..e0d9928f9 100644 --- a/src/pages/Claim/ClaimList.vue +++ b/src/pages/Claim/ClaimList.vue @@ -134,7 +134,7 @@ const columns = computed(() => [ const STATE_COLOR = { pending: 'bg-warning', - managed: 'bg-info', + loses: 'bg-negative', resolved: 'bg-positive', }; diff --git a/src/pages/Travel/ExtraCommunity.vue b/src/pages/Travel/ExtraCommunity.vue index d30629a80..ec898719d 100644 --- a/src/pages/Travel/ExtraCommunity.vue +++ b/src/pages/Travel/ExtraCommunity.vue @@ -18,7 +18,6 @@ import { usePrintService } from 'composables/usePrintService'; import VnSubToolbar from 'src/components/ui/VnSubToolbar.vue'; import axios from 'axios'; import RightMenu from 'src/components/common/RightMenu.vue'; -import VnPopup from 'src/components/common/VnPopup.vue'; const stateStore = useStateStore(); const { t } = useI18n(); @@ -183,7 +182,6 @@ const columns = computed(() => [ align: 'left', showValue: false, sortable: true, - style: 'min-width: 170px;', }, { label: t('globals.packages'), @@ -507,6 +505,7 @@ watch(route, () => { :props="props" @click="stopEventPropagation($event, col)" :style="col.style" + style="padding-left: 5px" > { {{ entry.volumeKg }} - - - - - - - - + + + {{ entry.evaNotes }} + @@ -643,7 +629,11 @@ watch(route, () => { } :deep(.q-table) { + table-layout: auto; + width: 100%; border-collapse: collapse; + overflow: hidden; + text-overflow: ellipsis; tbody tr td { &:nth-child(1) { diff --git a/src/router/__tests__/hooks.spec.js b/src/router/__tests__/hooks.spec.js new file mode 100644 index 000000000..97f5eacdc --- /dev/null +++ b/src/router/__tests__/hooks.spec.js @@ -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'; + +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(); + }); + }); +}); diff --git a/src/router/hooks.js b/src/router/hooks.js new file mode 100644 index 000000000..bd9e5334f --- /dev/null +++ b/src/router/hooks.js @@ -0,0 +1,95 @@ +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 { 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) { + 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 }, + ); + }); +} diff --git a/src/router/index.js b/src/router/index.js index 4403901cb..628a53c8e 100644 --- a/src/router/index.js +++ b/src/router/index.js @@ -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)); + Router.beforeEach(stateQueryGuard); + Router.afterEach(setPageTitle); Router.onError(({ message }) => { const errorMessages = [ diff --git a/test/cypress/integration/entry/commands.js b/test/cypress/integration/entry/commands.js index 7c96a5440..4d4a8f980 100644 --- a/test/cypress/integration/entry/commands.js +++ b/test/cypress/integration/entry/commands.js @@ -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', () => { diff --git a/test/cypress/integration/entry/entryCard/entryDescriptor.spec.js b/test/cypress/integration/entry/entryCard/entryDescriptor.spec.js index 554471008..8185866db 100644 --- a/test/cypress/integration/entry/entryCard/entryDescriptor.spec.js +++ b/test/cypress/integration/entry/entryCard/entryDescriptor.spec.js @@ -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(); cy.deleteEntry(); - cy.log(previousUrl); - cy.visit(previousUrl); cy.waitForElement('[data-cy="entry-buys"]');