diff --git a/packages/design-system/package.json b/packages/design-system/package.json
index 691d2c047f..f9de0a28f8 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 0000000000..d479bd5396
--- /dev/null
+++ b/packages/design-system/src/components/ConditionalRouterLink/CondtionalRouterLink.vue
@@ -0,0 +1,39 @@
+
+
+
+
+
diff --git a/packages/design-system/src/components/ConditionalRouterLink/__tests__/ConditionalRouterLink.spec.ts b/packages/design-system/src/components/ConditionalRouterLink/__tests__/ConditionalRouterLink.spec.ts
new file mode 100644
index 0000000000..853e06f8a9
--- /dev/null
+++ b/packages/design-system/src/components/ConditionalRouterLink/__tests__/ConditionalRouterLink.spec.ts
@@ -0,0 +1,67 @@
+import { render } from '@testing-library/vue';
+import { beforeAll, describe } from 'vitest';
+import { createRouter, createWebHistory } from 'vue-router';
+import CondtionalRouterLink from '../CondtionalRouterLink.vue';
+
+const slots = {
+ default: 'Button',
+};
+
+const router = createRouter({
+ history: createWebHistory(),
+ routes: [
+ {
+ path: '/',
+ name: 'home',
+ redirect: '/home',
+ },
+ ],
+});
+
+describe('CondtionalRouterLink', () => {
+ beforeAll(async () => {
+ await router.push('/');
+
+ await router.isReady();
+ });
+
+ it("renders router-link when 'to' prop is passed", () => {
+ const wrapper = render(CondtionalRouterLink, {
+ props: {
+ to: { name: 'home' },
+ },
+ slots,
+ global: {
+ plugins: [router],
+ },
+ });
+
+ expect(wrapper.html()).toMatchSnapshot();
+ });
+
+ it("renders when 'href' attr is passed", () => {
+ const wrapper = render(CondtionalRouterLink, {
+ attrs: {
+ href: 'https://n8n.io',
+ target: '_blank',
+ },
+ slots,
+ global: {
+ plugins: [router],
+ },
+ });
+
+ expect(wrapper.html()).toMatchSnapshot();
+ });
+
+ it('renders only the slot when neither to nor href is given', () => {
+ const wrapper = render(CondtionalRouterLink, {
+ slots,
+ global: {
+ plugins: [router],
+ },
+ });
+
+ expect(wrapper.html()).toMatchSnapshot();
+ });
+});
diff --git a/packages/design-system/src/components/ConditionalRouterLink/__tests__/__snapshots__/ConditionalRouterLink.spec.ts.snap b/packages/design-system/src/components/ConditionalRouterLink/__tests__/__snapshots__/ConditionalRouterLink.spec.ts.snap
new file mode 100644
index 0000000000..c316667540
--- /dev/null
+++ b/packages/design-system/src/components/ConditionalRouterLink/__tests__/__snapshots__/ConditionalRouterLink.spec.ts.snap
@@ -0,0 +1,7 @@
+// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
+
+exports[`CondtionalRouterLink > renders when 'href' attr is passed 1`] = `""`;
+
+exports[`CondtionalRouterLink > renders only the slot when neither to nor href is given 1`] = `"Button
"`;
+
+exports[`CondtionalRouterLink > renders router-link when 'to' prop is passed 1`] = `""`;
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 0000000000..4b8b5b65ad
--- /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 970d55b62f..bca1eff3bd 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 36e7dfbc98..9e4da3e1d6 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 5ad0429634..bb8df005a9 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 290b363b3d..603b833e30 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 0000000000..64627db787
--- /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 c97bfdd1f2..4a0a853b02 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 61fd657531..3861dbfce3 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 c183635e60..78be40f2ad 100644
--- a/packages/editor-ui/src/components/MainSidebar.vue
+++ b/packages/editor-ui/src/components/MainSidebar.vue
@@ -118,7 +118,6 @@ import { useUIStore } from '@/stores/ui.store';
import { useUsersStore } from '@/stores/users.store';
import { useVersionsStore } from '@/stores/versions.store';
import { useWorkflowsStore } from '@/stores/workflows.store';
-import { isNavigationFailure } from 'vue-router';
import ExecutionsUsage from '@/components/ExecutionsUsage.vue';
import BecomeTemplateCreatorCta from '@/components/BecomeTemplateCreatorCta/BecomeTemplateCreatorCta.vue';
import MainSidebarSourceControl from '@/components/MainSidebarSourceControl.vue';
@@ -205,38 +204,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 +230,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 +238,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 +246,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 +269,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 +281,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 +402,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 +422,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 84821d425d..6a7c1e03c3 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/components/__tests__/ExecutionsList.test.ts b/packages/editor-ui/src/components/__tests__/ExecutionsList.test.ts
index e66b6d43b3..ad20ade194 100644
--- a/packages/editor-ui/src/components/__tests__/ExecutionsList.test.ts
+++ b/packages/editor-ui/src/components/__tests__/ExecutionsList.test.ts
@@ -16,6 +16,7 @@ vi.mock('vue-router', () => ({
useRoute: vi.fn().mockReturnValue({
name: VIEWS.WORKFLOW_EXECUTIONS,
}),
+ RouterLink: vi.fn(),
}));
let pinia: ReturnType;
diff --git a/packages/editor-ui/src/components/__tests__/RBAC.test.ts b/packages/editor-ui/src/components/__tests__/RBAC.test.ts
index 566fe71294..63bd7a1a58 100644
--- a/packages/editor-ui/src/components/__tests__/RBAC.test.ts
+++ b/packages/editor-ui/src/components/__tests__/RBAC.test.ts
@@ -9,6 +9,7 @@ vi.mock('vue-router', () => ({
path: '/workflows',
params: {},
})),
+ RouterLink: vi.fn(),
}));
vi.mock('@/stores/rbac.store', () => ({
diff --git a/packages/editor-ui/src/composables/__tests__/useHistoryHelper.test.ts b/packages/editor-ui/src/composables/__tests__/useHistoryHelper.test.ts
index a035654b1f..304902aed2 100644
--- a/packages/editor-ui/src/composables/__tests__/useHistoryHelper.test.ts
+++ b/packages/editor-ui/src/composables/__tests__/useHistoryHelper.test.ts
@@ -31,6 +31,7 @@ vi.mock('@/stores/ui.store', () => {
});
vi.mock('vue-router', () => ({
useRoute: () => ({}),
+ RouterLink: vi.fn(),
}));
const TestComponent = defineComponent({
diff --git a/packages/editor-ui/src/router.ts b/packages/editor-ui/src/router.ts
index 0e4bc8ffa9..c8cb1c9d25 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 bfb12f54b8..fac4806621 100644
--- a/packages/editor-ui/src/stores/ui.store.ts
+++ b/packages/editor-ui/src/stores/ui.store.ts
@@ -44,7 +44,6 @@ import type {
CloudUpdateLinkSourceType,
CurlToJSONResponse,
IFakeDoorLocation,
- IMenuItem,
INodeUi,
IOnboardingCallPrompt,
IUser,
@@ -176,7 +175,6 @@ export const useUIStore = defineStore(STORES.UI, {
nodeViewOffsetPosition: [0, 0],
nodeViewMoveInProgress: false,
selectedNodes: [],
- sidebarMenuItems: [],
nodeViewInitialized: false,
addFirstStepOnLoad: false,
executionSidebarAutoRefresh: true,
@@ -528,10 +526,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/packages/editor-ui/src/views/__tests__/SamlOnboarding.test.ts b/packages/editor-ui/src/views/__tests__/SamlOnboarding.test.ts
index 13158b217e..148a11083e 100644
--- a/packages/editor-ui/src/views/__tests__/SamlOnboarding.test.ts
+++ b/packages/editor-ui/src/views/__tests__/SamlOnboarding.test.ts
@@ -14,6 +14,7 @@ vi.mock('vue-router', () => {
useRouter: () => ({
push,
}),
+ RouterLink: vi.fn(),
};
});
diff --git a/packages/editor-ui/src/views/__tests__/WorkflowHistory.test.ts b/packages/editor-ui/src/views/__tests__/WorkflowHistory.test.ts
index ad39e22bcc..a0f9fa2bde 100644
--- a/packages/editor-ui/src/views/__tests__/WorkflowHistory.test.ts
+++ b/packages/editor-ui/src/views/__tests__/WorkflowHistory.test.ts
@@ -33,6 +33,7 @@ vi.mock('vue-router', () => {
replace,
resolve,
}),
+ RouterLink: vi.fn(),
};
});
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 3fb9308c1c..5a583c874f 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