From 67fd89b35ed69f0767d9fa89c9949fc89480832d Mon Sep 17 00:00:00 2001 From: Tomi Turtiainen <10324676+tomi@users.noreply.github.com> Date: Thu, 18 Jan 2024 14:16:45 +0200 Subject: [PATCH] fix(editor): Use web native element in nav menus Currently all navigation in the menus is done programmatically. This is not accessible way to do navigation, as it prevents browser default behaviour, like cmd/ctrl+click to open into new tab. Also screen readers don't give any indication that the active element is an anchor. This PR refactors component library's Menu and MenuItem to use either vue-router RouterLink or when the menu item is a navigation element. --- packages/design-system/package.json | 1 + .../CondtionalRouterLink.vue | 44 +++++++ .../components/ConditionalRouterLink/index.ts | 3 + .../src/components/N8nMenu/Menu.stories.ts | 10 +- .../src/components/N8nMenu/Menu.vue | 26 +--- .../N8nMenuItem/MenuItem.stories.ts | 10 +- .../src/components/N8nMenuItem/MenuItem.vue | 80 ++++++------ .../src/components/N8nMenuItem/routerUtil.ts | 42 ++++++ packages/design-system/src/types/menu.ts | 26 +++- packages/editor-ui/src/Interface.ts | 1 - .../editor-ui/src/components/MainSidebar.vue | 121 ++++-------------- .../src/components/SettingsSidebar.vue | 70 ++-------- packages/editor-ui/src/router.ts | 114 ++++++++++------- packages/editor-ui/src/stores/ui.store.ts | 5 - pnpm-lock.yaml | 3 + 15 files changed, 269 insertions(+), 287 deletions(-) create mode 100644 packages/design-system/src/components/ConditionalRouterLink/CondtionalRouterLink.vue create mode 100644 packages/design-system/src/components/ConditionalRouterLink/index.ts create mode 100644 packages/design-system/src/components/N8nMenuItem/routerUtil.ts diff --git a/packages/design-system/package.json b/packages/design-system/package.json index 691d2c047f6ddf..f9de0a28f85ac0 100644 --- a/packages/design-system/package.json +++ b/packages/design-system/package.json @@ -74,6 +74,7 @@ "sanitize-html": "2.10.0", "vue": "^3.3.4", "vue-boring-avatars": "^1.3.0", + "vue-router": "^4.2.2", "xss": "^1.0.14" } } diff --git a/packages/design-system/src/components/ConditionalRouterLink/CondtionalRouterLink.vue b/packages/design-system/src/components/ConditionalRouterLink/CondtionalRouterLink.vue new file mode 100644 index 00000000000000..f6e4f4493b2241 --- /dev/null +++ b/packages/design-system/src/components/ConditionalRouterLink/CondtionalRouterLink.vue @@ -0,0 +1,44 @@ + + + diff --git a/packages/design-system/src/components/ConditionalRouterLink/index.ts b/packages/design-system/src/components/ConditionalRouterLink/index.ts new file mode 100644 index 00000000000000..4b8b5b65adb8e8 --- /dev/null +++ b/packages/design-system/src/components/ConditionalRouterLink/index.ts @@ -0,0 +1,3 @@ +import CondtionalRouterLink from './CondtionalRouterLink.vue'; + +export default CondtionalRouterLink; diff --git a/packages/design-system/src/components/N8nMenu/Menu.stories.ts b/packages/design-system/src/components/N8nMenu/Menu.stories.ts index 970d55b62ff05f..bca1eff3bd88a6 100644 --- a/packages/design-system/src/components/N8nMenu/Menu.stories.ts +++ b/packages/design-system/src/components/N8nMenu/Menu.stories.ts @@ -114,10 +114,9 @@ const menuItems = [ id: 'website', icon: 'globe', label: 'Website', - type: 'link', - properties: { + link: { href: 'https://www.n8n.io', - newWindow: true, + target: '_blank', }, position: 'bottom', }, @@ -140,10 +139,9 @@ const menuItems = [ id: 'quickstart', icon: 'video', label: 'Quickstart', - type: 'link', - properties: { + link: { href: 'https://www.youtube.com/watch?v=RpjQTGKm-ok', - newWindow: true, + target: '_blank', }, }, ], diff --git a/packages/design-system/src/components/N8nMenu/Menu.vue b/packages/design-system/src/components/N8nMenu/Menu.vue index 36e7dfbc98cd23..9e4da3e1d63692 100644 --- a/packages/design-system/src/components/N8nMenu/Menu.vue +++ b/packages/design-system/src/components/N8nMenu/Menu.vue @@ -59,6 +59,7 @@ import N8nMenuItem from '../N8nMenuItem'; import type { PropType } from 'vue'; import { defineComponent } from 'vue'; import type { IMenuItem, RouteObject } from '../../types'; +import { doesMenuItemMatchCurrentRoute } from '../N8nMenuItem/routerUtil'; export default defineComponent({ name: 'N8nMenu', @@ -128,14 +129,10 @@ export default defineComponent({ }, mounted() { if (this.mode === 'router') { - const found = this.items.find((item) => { - return ( - (Array.isArray(item.activateOnRouteNames) && - item.activateOnRouteNames.includes(this.currentRoute.name || '')) || - (Array.isArray(item.activateOnRoutePaths) && - item.activateOnRoutePaths.includes(this.currentRoute.path)) - ); - }); + const found = this.items.find((item) => + doesMenuItemMatchCurrentRoute(item, this.currentRoute), + ); + this.activeTab = found ? found.id : ''; } else { this.activeTab = this.items.length > 0 ? this.items[0].id : ''; @@ -145,19 +142,6 @@ export default defineComponent({ }, methods: { onSelect(item: IMenuItem): void { - if (item && item.type === 'link' && item.properties) { - const href: string = item.properties.href; - if (!href) { - return; - } - - if (item.properties.newWindow) { - window.open(href); - } else { - window.location.assign(item.properties.href); - } - } - if (this.mode === 'tabs') { this.activeTab = item.id; } diff --git a/packages/design-system/src/components/N8nMenuItem/MenuItem.stories.ts b/packages/design-system/src/components/N8nMenuItem/MenuItem.stories.ts index 5ad04296348eb0..bb8df005a917f5 100644 --- a/packages/design-system/src/components/N8nMenuItem/MenuItem.stories.ts +++ b/packages/design-system/src/components/N8nMenuItem/MenuItem.stories.ts @@ -75,10 +75,9 @@ link.args = { id: 'website', icon: 'globe', label: 'Website', - type: 'link', - properties: { + link: { href: 'https://www.n8n.io', - newWindow: true, + target: '_blank', }, }, }; @@ -96,10 +95,9 @@ withChildren.args = { id: 'quickstart', icon: 'video', label: 'Quickstart', - type: 'link', - properties: { + link: { href: 'https://www.youtube.com/watch?v=RpjQTGKm-ok', - newWindow: true, + target: '_blank', }, }, ], diff --git a/packages/design-system/src/components/N8nMenuItem/MenuItem.vue b/packages/design-system/src/components/N8nMenuItem/MenuItem.vue index 290b363b3dfb14..603b833e30d04a 100644 --- a/packages/design-system/src/components/N8nMenuItem/MenuItem.vue +++ b/packages/design-system/src/components/N8nMenuItem/MenuItem.vue @@ -40,37 +40,39 @@ :disabled="!compact" :show-after="tooltipDelay" > - - - {{ item.label }} - + - - - + + {{ item.label }} + + + + + @@ -81,7 +83,9 @@ import N8nTooltip from '../N8nTooltip'; import N8nIcon from '../N8nIcon'; import type { PropType } from 'vue'; import { defineComponent } from 'vue'; +import ConditionalRouterLink from '../ConditionalRouterLink'; import type { IMenuItem, RouteObject } from '../../types'; +import { doesMenuItemMatchCurrentRoute } from './routerUtil'; export default defineComponent({ name: 'N8nMenuItem', @@ -90,6 +94,7 @@ export default defineComponent({ ElMenuItem, N8nIcon, N8nTooltip, + ConditionalRouterLink, }, props: { item: { @@ -115,9 +120,11 @@ export default defineComponent({ }, activeTab: { type: String, + default: undefined, }, handleSelect: { type: Function as PropType<(item: IMenuItem) => void>, + default: undefined, }, }, computed: { @@ -151,18 +158,7 @@ export default defineComponent({ }, isActive(item: IMenuItem): boolean { if (this.mode === 'router') { - if (item.activateOnRoutePaths) { - return ( - Array.isArray(item.activateOnRoutePaths) && - item.activateOnRoutePaths.includes(this.currentRoute.path) - ); - } else if (item.activateOnRouteNames) { - return ( - Array.isArray(item.activateOnRouteNames) && - item.activateOnRouteNames.includes(this.currentRoute.name || '') - ); - } - return false; + return doesMenuItemMatchCurrentRoute(item, this.currentRoute); } else { return item.id === this.activeTab; } diff --git a/packages/design-system/src/components/N8nMenuItem/routerUtil.ts b/packages/design-system/src/components/N8nMenuItem/routerUtil.ts new file mode 100644 index 00000000000000..64627db7878e65 --- /dev/null +++ b/packages/design-system/src/components/N8nMenuItem/routerUtil.ts @@ -0,0 +1,42 @@ +import type { IMenuItem, RouteObject } from '@/types'; +import type { RouteLocationRaw } from 'vue-router'; + +/** + * Checks if the given menu item matches the current route. + */ +export function doesMenuItemMatchCurrentRoute(item: IMenuItem, currentRoute: RouteObject) { + let activateOnRouteNames: string[] = []; + if (Array.isArray(item.activateOnRouteNames)) { + activateOnRouteNames = item.activateOnRouteNames; + } else if (item.route && isNamedRouteLocation(item.route.to)) { + activateOnRouteNames = [item.route.to.name]; + } + + let activateOnRoutePaths: string[] = []; + if (Array.isArray(item.activateOnRoutePaths)) { + activateOnRoutePaths = item.activateOnRoutePaths; + } else if (item.route && isPathRouteLocation(item.route.to)) { + activateOnRoutePaths = [item.route.to.path]; + } + + return ( + activateOnRouteNames.includes(currentRoute.name ?? '') || + activateOnRoutePaths.includes(currentRoute.path) + ); +} + +function isPathRouteLocation(routeLocation?: RouteLocationRaw): routeLocation is { path: string } { + return ( + typeof routeLocation === 'object' && + 'path' in routeLocation && + typeof routeLocation.path === 'string' + ); +} + +function isNamedRouteLocation(routeLocation?: RouteLocationRaw): routeLocation is { name: string } { + return ( + typeof routeLocation === 'object' && + 'name' in routeLocation && + typeof routeLocation.name === 'string' + ); +} diff --git a/packages/design-system/src/types/menu.ts b/packages/design-system/src/types/menu.ts index c97bfdd1f21797..4a0a853b020bc9 100644 --- a/packages/design-system/src/types/menu.ts +++ b/packages/design-system/src/types/menu.ts @@ -1,4 +1,6 @@ import type { ElTooltipProps } from 'element-plus'; +import type { AnchorHTMLAttributes } from 'vue'; +import type { RouteLocationRaw, RouterLinkProps } from 'vue-router'; export type IMenuItem = { id: string; @@ -7,22 +9,32 @@ export type IMenuItem = { secondaryIcon?: { name: string; size?: 'xsmall' | 'small' | 'medium' | 'large' | 'xlarge'; - tooltip?: ElTooltipProps; + tooltip?: Partial; }; customIconSize?: 'medium' | 'small'; available?: boolean; position?: 'top' | 'bottom'; - type?: 'default' | 'link'; - properties?: ILinkMenuItemProperties; - // For router menus populate only one of those arrays: - // If menu item can be activated on certain route names (easy mode) + + /** Use this for external links */ + link?: ILinkMenuItemProperties; + /** Use this for defining a vue-router target */ + route?: RouterLinkProps; + /** + * If given, item will be activated on these route names. Note that if + * route is provided, it will be highlighted automatically + */ activateOnRouteNames?: string[]; - // For more specific matching, we can use paths activateOnRoutePaths?: string[]; + children?: IMenuItem[]; }; +export type IRouteMenuItemProperties = { + route: RouteLocationRaw; +}; + export type ILinkMenuItemProperties = { href: string; - newWindow?: boolean; + target?: AnchorHTMLAttributes['target']; + rel?: AnchorHTMLAttributes['rel']; }; diff --git a/packages/editor-ui/src/Interface.ts b/packages/editor-ui/src/Interface.ts index 61fd657531e7be..3861dbfce3993f 100644 --- a/packages/editor-ui/src/Interface.ts +++ b/packages/editor-ui/src/Interface.ts @@ -1270,7 +1270,6 @@ export interface UIState { nodeViewOffsetPosition: XYPosition; nodeViewMoveInProgress: boolean; selectedNodes: INodeUi[]; - sidebarMenuItems: IMenuItem[]; nodeViewInitialized: boolean; addFirstStepOnLoad: boolean; executionSidebarAutoRefresh: boolean; diff --git a/packages/editor-ui/src/components/MainSidebar.vue b/packages/editor-ui/src/components/MainSidebar.vue index c183635e60dce3..a4cef0062d418d 100644 --- a/packages/editor-ui/src/components/MainSidebar.vue +++ b/packages/editor-ui/src/components/MainSidebar.vue @@ -205,38 +205,24 @@ export default defineComponent({ }, mainMenuItems(): IMenuItem[] { const items: IMenuItem[] = []; - const injectedItems = this.uiStore.sidebarMenuItems; const workflows: IMenuItem = { id: 'workflows', icon: 'network-wired', label: this.$locale.baseText('mainSidebar.workflows'), position: 'top', - activateOnRouteNames: [VIEWS.WORKFLOWS], + route: { to: { name: VIEWS.WORKFLOWS } }, + secondaryIcon: this.sourceControlStore.preferences.branchReadOnly + ? { + name: 'lock', + tooltip: { + content: this.$locale.baseText('mainSidebar.workflows.readOnlyEnv.tooltip'), + }, + } + : undefined, }; - if (this.sourceControlStore.preferences.branchReadOnly) { - workflows.secondaryIcon = { - name: 'lock', - tooltip: { - content: this.$locale.baseText('mainSidebar.workflows.readOnlyEnv.tooltip'), - }, - }; - } - - if (injectedItems && injectedItems.length > 0) { - for (const item of injectedItems) { - items.push({ - id: item.id, - icon: item.icon || '', - label: item.label || '', - position: item.position, - type: item.properties?.href ? 'link' : 'regular', - properties: item.properties, - } as IMenuItem); - } - } - + const defaultSettingsRoute = this.findFirstAccessibleSettingsRoute(); const regularItems: IMenuItem[] = [ workflows, { @@ -245,7 +231,7 @@ export default defineComponent({ label: this.$locale.baseText('mainSidebar.templates'), position: 'top', available: this.settingsStore.isTemplatesEnabled, - activateOnRouteNames: [VIEWS.TEMPLATES], + route: { to: { name: VIEWS.TEMPLATES } }, }, { id: 'credentials', @@ -253,7 +239,7 @@ export default defineComponent({ label: this.$locale.baseText('mainSidebar.credentials'), customIconSize: 'medium', position: 'top', - activateOnRouteNames: [VIEWS.CREDENTIALS], + route: { to: { name: VIEWS.CREDENTIALS } }, }, { id: 'variables', @@ -261,18 +247,17 @@ export default defineComponent({ label: this.$locale.baseText('mainSidebar.variables'), customIconSize: 'medium', position: 'top', - activateOnRouteNames: [VIEWS.VARIABLES], + route: { to: { name: VIEWS.VARIABLES } }, }, { id: 'executions', icon: 'tasks', label: this.$locale.baseText('mainSidebar.executions'), position: 'top', - activateOnRouteNames: [VIEWS.EXECUTIONS], + route: { to: { name: VIEWS.EXECUTIONS } }, }, { id: 'cloud-admin', - type: 'link', position: 'bottom', label: 'Admin Panel', icon: 'home', @@ -285,6 +270,7 @@ export default defineComponent({ position: 'bottom', available: this.canUserAccessSettings && this.usersStore.currentUser !== null, activateOnRouteNames: [VIEWS.USERS_SETTINGS, VIEWS.API_SETTINGS, VIEWS.PERSONAL_SETTINGS], + route: { to: defaultSettingsRoute }, }, { id: 'help', @@ -296,40 +282,36 @@ export default defineComponent({ id: 'quickstart', icon: 'video', label: this.$locale.baseText('mainSidebar.helpMenuItems.quickstart'), - type: 'link', - properties: { + link: { href: 'https://www.youtube.com/watch?v=1MwSoB0gnM4', - newWindow: true, + target: '_blank', }, }, { id: 'docs', icon: 'book', label: this.$locale.baseText('mainSidebar.helpMenuItems.documentation'), - type: 'link', - properties: { + link: { href: 'https://docs.n8n.io?utm_source=n8n_app&utm_medium=app_sidebar', - newWindow: true, + target: '_blank', }, }, { id: 'forum', icon: 'users', label: this.$locale.baseText('mainSidebar.helpMenuItems.forum'), - type: 'link', - properties: { + link: { href: 'https://community.n8n.io?utm_source=n8n_app&utm_medium=app_sidebar', - newWindow: true, + target: '_blank', }, }, { id: 'examples', icon: 'graduation-cap', label: this.$locale.baseText('mainSidebar.helpMenuItems.course'), - type: 'link', - properties: { + link: { href: 'https://www.youtube.com/watch?v=1MwSoB0gnM4', - newWindow: true, + target: '_blank', }, }, { @@ -421,46 +403,6 @@ export default defineComponent({ }, async handleSelect(key: string) { switch (key) { - case 'workflows': { - if (this.$router.currentRoute.value.name !== VIEWS.WORKFLOWS) { - this.goToRoute({ name: VIEWS.WORKFLOWS }); - } - break; - } - case 'templates': { - if (this.$router.currentRoute.value.name !== VIEWS.TEMPLATES) { - this.goToRoute({ name: VIEWS.TEMPLATES }); - } - break; - } - case 'credentials': { - if (this.$router.currentRoute.value.name !== VIEWS.CREDENTIALS) { - this.goToRoute({ name: VIEWS.CREDENTIALS }); - } - break; - } - case 'variables': { - if (this.$router.currentRoute.value.name !== VIEWS.VARIABLES) { - this.goToRoute({ name: VIEWS.VARIABLES }); - } - break; - } - case 'executions': { - if (this.$router.currentRoute.value.name !== VIEWS.EXECUTIONS) { - this.goToRoute({ name: VIEWS.EXECUTIONS }); - } - break; - } - case 'settings': { - const defaultRoute = this.findFirstAccessibleSettingsRoute(); - if (defaultRoute) { - const route = this.$router.resolve({ name: defaultRoute }); - if (this.$router.currentRoute.value.name !== defaultRoute) { - this.goToRoute(route.path); - } - } - break; - } case 'about': { this.trackHelpItemClick('about'); this.uiStore.openModal(ABOUT_MODAL_KEY); @@ -481,25 +423,18 @@ export default defineComponent({ break; } }, - goToRoute(route: string | { name: string }) { - this.$router.push(route).catch((failure) => { - console.log(failure); - // Catch navigation failures caused by route guards - if (!isNavigationFailure(failure)) { - console.error(failure); - } - }); - }, findFirstAccessibleSettingsRoute() { const settingsRoutes = this.$router .getRoutes() .find((route) => route.path === '/settings')! - .children.map((route) => route.name || ''); + .children.map((route) => route.name ?? ''); - let defaultSettingsRoute = null; + let defaultSettingsRoute = { name: VIEWS.USERS_SETTINGS }; for (const route of settingsRoutes) { if (this.canUserAccessRouteByName(route.toString())) { - defaultSettingsRoute = route; + defaultSettingsRoute = { + name: route.toString() as VIEWS, + }; break; } } diff --git a/packages/editor-ui/src/components/SettingsSidebar.vue b/packages/editor-ui/src/components/SettingsSidebar.vue index 84821d425dc4be..6a7c1e03c387c5 100644 --- a/packages/editor-ui/src/components/SettingsSidebar.vue +++ b/packages/editor-ui/src/components/SettingsSidebar.vue @@ -49,7 +49,7 @@ export default defineComponent({ label: this.$locale.baseText('settings.usageAndPlan.title'), position: 'top', available: this.canAccessUsageAndPlan(), - activateOnRouteNames: [VIEWS.USAGE], + route: { to: { name: VIEWS.USAGE } }, }, { id: 'settings-personal', @@ -57,7 +57,7 @@ export default defineComponent({ label: this.$locale.baseText('settings.personal'), position: 'top', available: this.canAccessPersonalSettings(), - activateOnRouteNames: [VIEWS.PERSONAL_SETTINGS], + route: { to: { name: VIEWS.PERSONAL_SETTINGS } }, }, { id: 'settings-users', @@ -65,7 +65,7 @@ export default defineComponent({ label: this.$locale.baseText('settings.users'), position: 'top', available: this.canAccessUsersSettings(), - activateOnRouteNames: [VIEWS.USERS_SETTINGS], + route: { to: { name: VIEWS.USERS_SETTINGS } }, }, { id: 'settings-api', @@ -73,7 +73,7 @@ export default defineComponent({ label: this.$locale.baseText('settings.n8napi'), position: 'top', available: this.canAccessApiSettings(), - activateOnRouteNames: [VIEWS.API_SETTINGS], + route: { to: { name: VIEWS.API_SETTINGS } }, }, { id: 'settings-external-secrets', @@ -81,10 +81,7 @@ export default defineComponent({ label: this.$locale.baseText('settings.externalSecrets.title'), position: 'top', available: this.canAccessExternalSecrets(), - activateOnRouteNames: [ - VIEWS.EXTERNAL_SECRETS_SETTINGS, - VIEWS.EXTERNAL_SECRETS_PROVIDER_SETTINGS, - ], + route: { to: { name: VIEWS.EXTERNAL_SECRETS_SETTINGS } }, }, { id: 'settings-audit-logs', @@ -92,7 +89,7 @@ export default defineComponent({ label: this.$locale.baseText('settings.auditLogs.title'), position: 'top', available: this.canAccessAuditLogs(), - activateOnRouteNames: [VIEWS.AUDIT_LOGS], + route: { to: { name: VIEWS.AUDIT_LOGS } }, }, { id: 'settings-source-control', @@ -100,7 +97,7 @@ export default defineComponent({ label: this.$locale.baseText('settings.sourceControl.title'), position: 'top', available: this.canAccessSourceControl(), - activateOnRouteNames: [VIEWS.SOURCE_CONTROL], + route: { to: { name: VIEWS.SOURCE_CONTROL } }, }, { id: 'settings-sso', @@ -108,7 +105,7 @@ export default defineComponent({ label: this.$locale.baseText('settings.sso'), position: 'top', available: this.canAccessSso(), - activateOnRouteNames: [VIEWS.SSO_SETTINGS], + route: { to: { name: VIEWS.SSO_SETTINGS } }, }, { id: 'settings-ldap', @@ -116,7 +113,7 @@ export default defineComponent({ label: this.$locale.baseText('settings.ldap'), position: 'top', available: this.canAccessLdapSettings(), - activateOnRouteNames: [VIEWS.LDAP_SETTINGS], + route: { to: { name: VIEWS.LDAP_SETTINGS } }, }, { id: 'settings-workersview', @@ -126,7 +123,7 @@ export default defineComponent({ available: this.settingsStore.isQueueModeEnabled && hasPermission(['rbac'], { rbac: { scope: 'workersView:manage' } }), - activateOnRouteNames: [VIEWS.WORKER_VIEW], + route: { to: { name: VIEWS.WORKER_VIEW } }, }, ]; @@ -134,7 +131,7 @@ export default defineComponent({ if (item.uiLocations.includes('settings')) { menuItems.push({ id: item.id, - icon: item.icon || 'question', + icon: item.icon ?? 'question', label: this.$locale.baseText(item.featureName as BaseTextKey), position: 'top', available: true, @@ -149,7 +146,7 @@ export default defineComponent({ label: this.$locale.baseText('settings.log-streaming'), position: 'top', available: this.canAccessLogStreamingSettings(), - activateOnRouteNames: [VIEWS.LOG_STREAMING_SETTINGS], + route: { to: { name: VIEWS.LOG_STREAMING_SETTINGS } }, }); menuItems.push({ @@ -158,7 +155,7 @@ export default defineComponent({ label: this.$locale.baseText('settings.communityNodes'), position: 'top', available: this.canAccessCommunityNodes(), - activateOnRouteNames: [VIEWS.COMMUNITY_NODES], + route: { to: { name: VIEWS.COMMUNITY_NODES } }, }); return menuItems; @@ -211,51 +208,10 @@ export default defineComponent({ }, async handleSelect(key: string) { switch (key) { - case 'settings-personal': - await this.navigateTo(VIEWS.PERSONAL_SETTINGS); - break; - case 'settings-users': - await this.navigateTo(VIEWS.USERS_SETTINGS); - break; - case 'settings-api': - await this.navigateTo(VIEWS.API_SETTINGS); - break; - case 'settings-ldap': - await this.navigateTo(VIEWS.LDAP_SETTINGS); - break; - case 'settings-log-streaming': - await this.navigateTo(VIEWS.LOG_STREAMING_SETTINGS); - break; case 'users': // Fakedoor feature added via hooks when user management is disabled on cloud case 'logging': this.$router.push({ name: VIEWS.FAKE_DOOR, params: { featureId: key } }).catch(() => {}); break; - case 'settings-community-nodes': - await this.navigateTo(VIEWS.COMMUNITY_NODES); - break; - case 'settings-usage-and-plan': - await this.navigateTo(VIEWS.USAGE); - break; - case 'settings-sso': - await this.navigateTo(VIEWS.SSO_SETTINGS); - break; - case 'settings-external-secrets': - await this.navigateTo(VIEWS.EXTERNAL_SECRETS_SETTINGS); - break; - case 'settings-source-control': - if (this.$router.currentRoute.name !== VIEWS.SOURCE_CONTROL) { - void this.$router.push({ name: VIEWS.SOURCE_CONTROL }); - } - break; - case 'settings-audit-logs': - if (this.$router.currentRoute.name !== VIEWS.AUDIT_LOGS) { - void this.$router.push({ name: VIEWS.AUDIT_LOGS }); - } - break; - case 'settings-workersview': { - await this.navigateTo(VIEWS.WORKER_VIEW); - break; - } default: break; } diff --git a/packages/editor-ui/src/router.ts b/packages/editor-ui/src/router.ts index 0e4bc8ffa96cd2..c8cb1c9d254bba 100644 --- a/packages/editor-ui/src/router.ts +++ b/packages/editor-ui/src/router.ts @@ -7,7 +7,7 @@ import type { RouteLocationRaw, RouteLocationNormalized, } from 'vue-router'; -import { createRouter, createWebHistory } from 'vue-router'; +import { createRouter, createWebHistory, isNavigationFailure } from 'vue-router'; import { useExternalHooks } from '@/composables/useExternalHooks'; import { useSettingsStore } from '@/stores/settings.store'; import { useTemplatesStore } from '@/stores/templates.store'; @@ -784,73 +784,89 @@ const router = createRouter({ }); router.beforeEach(async (to: RouteLocationNormalized & RouteConfig, from, next) => { - /** - * Initialize application core - * This step executes before first route is loaded and is required for permission checks - */ + try { + /** + * Initialize application core + * This step executes before first route is loaded and is required for permission checks + */ - await initializeCore(); + await initializeCore(); - /** - * Redirect to setup page. User should be redirected to this only once - */ + /** + * Redirect to setup page. User should be redirected to this only once + */ - const settingsStore = useSettingsStore(); - if (settingsStore.showSetupPage) { - if (to.name === VIEWS.SETUP) { - return next(); + const settingsStore = useSettingsStore(); + if (settingsStore.showSetupPage) { + if (to.name === VIEWS.SETUP) { + return next(); + } + + return next({ name: VIEWS.SETUP }); } - return next({ name: VIEWS.SETUP }); - } + /** + * Verify user permissions for current route + */ - /** - * Verify user permissions for current route - */ + const routeMiddleware = to.meta?.middleware ?? []; + const routeMiddlewareOptions = to.meta?.middlewareOptions ?? {}; + for (const middlewareName of routeMiddleware) { + let nextCalled = false; + const middlewareNext = ((location: RouteLocationRaw): void => { + next(location); + nextCalled = true; + }) as NavigationGuardNext; - const routeMiddleware = to.meta?.middleware ?? []; - const routeMiddlewareOptions = to.meta?.middlewareOptions ?? {}; - for (const middlewareName of routeMiddleware) { - let nextCalled = false; - const middlewareNext = ((location: RouteLocationRaw): void => { - next(location); - nextCalled = true; - }) as NavigationGuardNext; + const middlewareOptions = routeMiddlewareOptions[middlewareName]; + const middlewareFn = middleware[middlewareName] as RouterMiddleware; + await middlewareFn(to, from, middlewareNext, middlewareOptions); - const middlewareOptions = routeMiddlewareOptions[middlewareName]; - const middlewareFn = middleware[middlewareName] as RouterMiddleware; - await middlewareFn(to, from, middlewareNext, middlewareOptions); + if (nextCalled) { + return; + } + } - if (nextCalled) { - return; + return next(); + } catch (failure) { + if (isNavigationFailure(failure)) { + console.log(failure); + } else { + console.error(failure); } } - - return next(); }); router.afterEach((to, from) => { - const telemetry = useTelemetry(); - const uiStore = useUIStore(); - const templatesStore = useTemplatesStore(); + try { + const telemetry = useTelemetry(); + const uiStore = useUIStore(); + const templatesStore = useTemplatesStore(); - /** - * Run external hooks - */ + /** + * Run external hooks + */ - void useExternalHooks().run('main.routeChange', { from, to }); + void useExternalHooks().run('main.routeChange', { from, to }); - /** - * Track current view for telemetry - */ + /** + * Track current view for telemetry + */ - uiStore.currentView = (to.name as string) ?? ''; - if (to.meta?.templatesEnabled) { - templatesStore.setSessionId(); - } else { - templatesStore.resetSessionId(); // reset telemetry session id when user leaves template pages + uiStore.currentView = (to.name as string) ?? ''; + if (to.meta?.templatesEnabled) { + templatesStore.setSessionId(); + } else { + templatesStore.resetSessionId(); // reset telemetry session id when user leaves template pages + } + telemetry.page(to); + } catch (failure) { + if (isNavigationFailure(failure)) { + console.log(failure); + } else { + console.error(failure); + } } - telemetry.page(to); }); export default router; diff --git a/packages/editor-ui/src/stores/ui.store.ts b/packages/editor-ui/src/stores/ui.store.ts index bfb12f54b8b092..e57bba1a09513f 100644 --- a/packages/editor-ui/src/stores/ui.store.ts +++ b/packages/editor-ui/src/stores/ui.store.ts @@ -176,7 +176,6 @@ export const useUIStore = defineStore(STORES.UI, { nodeViewOffsetPosition: [0, 0], nodeViewMoveInProgress: false, selectedNodes: [], - sidebarMenuItems: [], nodeViewInitialized: false, addFirstStepOnLoad: false, executionSidebarAutoRefresh: true, @@ -528,10 +527,6 @@ export const useUIStore = defineStore(STORES.UI, { resetSelectedNodes(): void { this.selectedNodes = []; }, - addSidebarMenuItems(menuItems: IMenuItem[]) { - const updated = this.sidebarMenuItems.concat(menuItems); - this.sidebarMenuItems = updated; - }, setCurlCommand(payload: { name: string; command: string }): void { this.modals[payload.name] = { ...this.modals[payload.name], diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 711659fa28d362..142171e30f58a0 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -867,6 +867,9 @@ importers: vue-boring-avatars: specifier: ^1.3.0 version: 1.3.0(vue@3.3.4) + vue-router: + specifier: ^4.2.2 + version: 4.2.2(vue@3.3.4) xss: specifier: ^1.0.14 version: 1.0.14