mirror of
https://github.com/n8n-io/n8n.git
synced 2026-10-17 05:29:44 +02:00
feat(editor): Add order to settings sidebar links (#40505)
Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
a2c8142013
commit
cef87cfcab
|
|
@ -192,6 +192,14 @@ you use a surface.
|
|||
| `settingsPages` | `registerModuleSettingsPages` | `SettingsSidebar` |
|
||||
| `pushHandlers` | `registerModulePushHandlers` | `useModulePushDispatcher`, in `App.vue` |
|
||||
|
||||
### Position of a settings link
|
||||
|
||||
Each item in `settingsPages` accepts an optional integer `order`. The sidebar sorts the shell links
|
||||
and the module links together, from low to high. The shell links use 10, 20, 30, and so on. Pick a
|
||||
number between two shell links to put your link between them. A link without `order` goes last. Two
|
||||
links with the same `order` keep their registration order. The `order` field is not part of
|
||||
`IMenuItem`.
|
||||
|
||||
All the register functions are in `editor-ui/src/app/moduleInitializer/moduleInitializer.ts`.
|
||||
`main.ts` registers `routes` before the mount. `app/init/index.ts` registers the other surfaces
|
||||
after the login.
|
||||
|
|
|
|||
|
|
@ -12,6 +12,16 @@ import type { ModuleSetupContext } from './setup';
|
|||
import type { ModuleShortcut } from './shortcut';
|
||||
import type { DynamicTabOptions } from './tabs';
|
||||
|
||||
/** A link in the settings sidebar. Shell and modules both use it. */
|
||||
export type IMenuSettingItem = IMenuItem & {
|
||||
/**
|
||||
* @description Sort position, low to high. Shell links use 10, 20, 30, ... so pick a number
|
||||
* between two of them to land between them. No `order` means last. Equal values
|
||||
* keep registration order.
|
||||
*/
|
||||
order?: number;
|
||||
};
|
||||
|
||||
/**
|
||||
* The declarative contract a frontend module exposes to the editor-ui shell.
|
||||
*
|
||||
|
|
@ -37,7 +47,7 @@ export type FrontendModuleDescription = {
|
|||
* here so they arrive on the same path as `modals`, not by import side effect.
|
||||
*/
|
||||
adHocModalKeyPrefixes?: string[];
|
||||
settingsPages?: IMenuItem[];
|
||||
settingsPages?: IMenuSettingItem[];
|
||||
|
||||
// --- descriptor v2 (all optional, additive) ---
|
||||
|
||||
|
|
|
|||
|
|
@ -3,13 +3,17 @@ import { computed, ref } from 'vue';
|
|||
import { PROMOTIONS_SETTINGS_VIEW } from '@/features/integrations/promotions.ee/promotions.constants';
|
||||
import { useSettingsItems } from './useSettingsItems';
|
||||
import { VIEWS } from '../constants';
|
||||
import { hasPermission } from '../utils/rbac/permissions';
|
||||
|
||||
const isAiGatewayCloudUbbEnabled = ref(false);
|
||||
const isAiGatewayEnabled = ref(true);
|
||||
const isAiAssistantEnabled = ref(false);
|
||||
const isPublicApiEnabled = ref(false);
|
||||
const isQueueModeEnabled = ref(false);
|
||||
const balance = ref<number>();
|
||||
const moduleSettings = ref<Record<string, unknown>>({});
|
||||
// `ui.store` stamps `available: true` onto every module item before exposing it.
|
||||
const settingsSidebarItems = ref<Array<{ id: string; available: boolean }>>([]);
|
||||
const settingsSidebarItems = ref<Array<{ id: string; available: boolean; order?: number }>>([]);
|
||||
const activeModules = ref<string[]>([]);
|
||||
const promotionsFlag = ref('false');
|
||||
const canUserAccessRouteByName = vi.hoisted(() => vi.fn<(name: string) => boolean>(() => true));
|
||||
|
|
@ -39,15 +43,21 @@ vi.mock('../stores/ui.store', () => ({
|
|||
}));
|
||||
vi.mock('@n8n/stores/settings.store', () => ({
|
||||
useSettingsStore: vi.fn(() => ({
|
||||
isAiAssistantEnabled: false,
|
||||
get isAiAssistantEnabled() {
|
||||
return isAiAssistantEnabled.value;
|
||||
},
|
||||
get isAiGatewayEnabled() {
|
||||
return isAiGatewayEnabled.value;
|
||||
},
|
||||
get isAiGatewayCloudUbbEnabled() {
|
||||
return isAiGatewayCloudUbbEnabled.value;
|
||||
},
|
||||
isPublicApiEnabled: false,
|
||||
isQueueModeEnabled: false,
|
||||
get isPublicApiEnabled() {
|
||||
return isPublicApiEnabled.value;
|
||||
},
|
||||
get isQueueModeEnabled() {
|
||||
return isQueueModeEnabled.value;
|
||||
},
|
||||
isModuleActive: (name: string) => activeModules.value.includes(name),
|
||||
get settings() {
|
||||
return { envFeatureFlags: { N8N_ENV_FEAT_PROMOTIONS: promotionsFlag.value } };
|
||||
|
|
@ -64,6 +74,9 @@ describe('useSettingsItems', () => {
|
|||
vi.clearAllMocks();
|
||||
isAiGatewayEnabled.value = true;
|
||||
isAiGatewayCloudUbbEnabled.value = false;
|
||||
isAiAssistantEnabled.value = false;
|
||||
isPublicApiEnabled.value = false;
|
||||
isQueueModeEnabled.value = false;
|
||||
balance.value = undefined;
|
||||
moduleSettings.value = {};
|
||||
settingsSidebarItems.value = [];
|
||||
|
|
@ -73,19 +86,102 @@ describe('useSettingsItems', () => {
|
|||
contextPreferencesEnabled.value = true;
|
||||
});
|
||||
|
||||
describe('sidebar order', () => {
|
||||
it('lists every link in the established order when all are available', () => {
|
||||
isAiAssistantEnabled.value = true;
|
||||
isPublicApiEnabled.value = true;
|
||||
isQueueModeEnabled.value = true;
|
||||
moduleSettings.value = { 'encryption-key-manager': { rotationEnabled: true } };
|
||||
activeModules.value = ['promotions'];
|
||||
promotionsFlag.value = 'true';
|
||||
vi.mocked(hasPermission).mockReturnValue(true);
|
||||
// Module items in registration order, with the orders the real modules set.
|
||||
settingsSidebarItems.value = [
|
||||
{ id: 'settings-mcp', available: true, order: 200 },
|
||||
{ id: 'settings-chat-hub', available: true, order: 220 },
|
||||
{ id: 'settings-instance-ai', available: true, order: 230 },
|
||||
{ id: 'settings-agents', available: true, order: 240 },
|
||||
{ id: 'settings-opentelemetry', available: true },
|
||||
];
|
||||
|
||||
const ids = useSettingsItems().settingsItems.value.map(({ id }) => id);
|
||||
|
||||
expect(ids).toEqual([
|
||||
'settings-usage-and-plan',
|
||||
'settings-personal',
|
||||
'settings-users',
|
||||
'settings-ai',
|
||||
'settings-n8n-connect',
|
||||
'settings-roles',
|
||||
'settings-api',
|
||||
'settings-external-secrets',
|
||||
'settings-credential-resolvers',
|
||||
'settings-source-control',
|
||||
'settings-promotions',
|
||||
'settings-sso',
|
||||
'settings-encryption-keys',
|
||||
'settings-security',
|
||||
'settings-ldap',
|
||||
'settings-workersview',
|
||||
'settings-log-streaming',
|
||||
'settings-community-nodes',
|
||||
'settings-migration-report',
|
||||
'settings-mcp',
|
||||
'settings-context',
|
||||
'settings-chat-hub',
|
||||
'settings-instance-ai',
|
||||
'settings-agents',
|
||||
'settings-opentelemetry',
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('module item order', () => {
|
||||
const idsOf = () => useSettingsItems().settingsItems.value.map(({ id }) => id);
|
||||
|
||||
it('places a module item between the shell items with the nearest orders', () => {
|
||||
settingsSidebarItems.value = [{ id: 'settings-module', available: true, order: 25 }];
|
||||
|
||||
const ids = idsOf();
|
||||
|
||||
expect(ids.indexOf('settings-module')).toBe(ids.indexOf('settings-personal') + 1);
|
||||
expect(ids.indexOf('settings-module')).toBe(ids.indexOf('settings-users') - 1);
|
||||
});
|
||||
|
||||
it('places a module item without order last', () => {
|
||||
settingsSidebarItems.value = [{ id: 'settings-module', available: true }];
|
||||
|
||||
expect(idsOf().at(-1)).toBe('settings-module');
|
||||
});
|
||||
|
||||
it('keeps registration order for equal orders', () => {
|
||||
settingsSidebarItems.value = [
|
||||
{ id: 'settings-first', available: true, order: 25 },
|
||||
{ id: 'settings-second', available: true, order: 25 },
|
||||
{ id: 'settings-third', available: true },
|
||||
{ id: 'settings-fourth', available: true },
|
||||
];
|
||||
|
||||
const ids = idsOf();
|
||||
|
||||
expect(ids.indexOf('settings-second')).toBe(ids.indexOf('settings-first') + 1);
|
||||
expect(ids.indexOf('settings-fourth')).toBe(ids.indexOf('settings-third') + 1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('the Context item', () => {
|
||||
const idsOf = () => useSettingsItems().settingsItems.value.map(({ id }) => id);
|
||||
|
||||
it('sits directly after the module-registered MCP item', () => {
|
||||
it('sorts between module items with a lower and a higher order', () => {
|
||||
settingsSidebarItems.value = [
|
||||
{ id: 'settings-mcp', available: true },
|
||||
{ id: 'settings-chat', available: true },
|
||||
{ id: 'settings-mcp', available: true, order: 200 },
|
||||
{ id: 'settings-chat-hub', available: true, order: 220 },
|
||||
];
|
||||
|
||||
const ids = idsOf();
|
||||
|
||||
expect(ids.indexOf('settings-context')).toBe(ids.indexOf('settings-mcp') + 1);
|
||||
expect(ids.indexOf('settings-context')).toBeLessThan(ids.indexOf('settings-chat'));
|
||||
expect(ids.indexOf('settings-context')).toBe(ids.indexOf('settings-chat-hub') - 1);
|
||||
});
|
||||
|
||||
it('is hidden when the flag is off, because the route guard does not run here', () => {
|
||||
|
|
@ -94,13 +190,10 @@ describe('useSettingsItems', () => {
|
|||
expect(idsOf()).not.toContain('settings-context');
|
||||
});
|
||||
|
||||
it('falls back to the end when the MCP module is inactive', () => {
|
||||
it('is last when no module item is registered', () => {
|
||||
settingsSidebarItems.value = [];
|
||||
|
||||
const ids = idsOf();
|
||||
|
||||
expect(ids).not.toContain('settings-mcp');
|
||||
expect(ids.at(-1)).toBe('settings-context');
|
||||
expect(idsOf().at(-1)).toBe('settings-context');
|
||||
});
|
||||
|
||||
it('carries the preview label', () => {
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ import { useUserHelpers } from './useUserHelpers';
|
|||
import { useAiGateway } from './useAiGateway';
|
||||
import { useAiGatewayTopUp } from './useAiGatewayTopUp';
|
||||
import { computed } from 'vue';
|
||||
import type { IMenuItem } from '@n8n/design-system';
|
||||
import type { IMenuSettingItem } from '@n8n/frontend-module-sdk';
|
||||
import { useI18n } from '@n8n/i18n';
|
||||
import { VIEWS } from '../constants';
|
||||
import { isContextPreferencesEnabled } from '@/features/settings/context/context.utils';
|
||||
|
|
@ -14,6 +14,8 @@ import { MIGRATION_REPORT_TARGET_VERSION } from '@n8n/api-types';
|
|||
import { PROMOTIONS_SETTINGS_VIEW } from '@/features/integrations/promotions.ee/promotions.constants';
|
||||
import { usePromotionsEnabled } from '@/features/shared/promotions/usePromotionsEnabled';
|
||||
|
||||
type ShellSettingsItem = IMenuSettingItem & { order: number };
|
||||
|
||||
export function useSettingsItems() {
|
||||
const router = useRouter();
|
||||
const i18n = useI18n();
|
||||
|
|
@ -24,10 +26,11 @@ export function useSettingsItems() {
|
|||
const { balance } = useAiGateway();
|
||||
const { openTopUp } = useAiGatewayTopUp();
|
||||
|
||||
const settingsItems = computed<IMenuItem[]>(() => {
|
||||
const menuItems: IMenuItem[] = [
|
||||
const settingsItems = computed<IMenuSettingItem[]>(() => {
|
||||
const shellItems: ShellSettingsItem[] = [
|
||||
{
|
||||
id: 'settings-usage-and-plan',
|
||||
order: 10,
|
||||
icon: 'chart-column-decreasing',
|
||||
label: i18n.baseText('settings.usageAndPlan.title'),
|
||||
position: 'top',
|
||||
|
|
@ -36,6 +39,7 @@ export function useSettingsItems() {
|
|||
},
|
||||
{
|
||||
id: 'settings-personal',
|
||||
order: 20,
|
||||
icon: 'circle-user-round',
|
||||
label: i18n.baseText('settings.personal'),
|
||||
position: 'top',
|
||||
|
|
@ -44,6 +48,7 @@ export function useSettingsItems() {
|
|||
},
|
||||
{
|
||||
id: 'settings-users',
|
||||
order: 30,
|
||||
icon: 'user-round',
|
||||
label: i18n.baseText('settings.users'),
|
||||
position: 'top',
|
||||
|
|
@ -52,6 +57,7 @@ export function useSettingsItems() {
|
|||
},
|
||||
{
|
||||
id: 'settings-ai',
|
||||
order: 40,
|
||||
icon: 'sparkles',
|
||||
label: i18n.baseText('settings.ai'),
|
||||
position: 'top',
|
||||
|
|
@ -61,6 +67,7 @@ export function useSettingsItems() {
|
|||
},
|
||||
{
|
||||
id: 'settings-n8n-connect',
|
||||
order: 50,
|
||||
icon: 'plug-zap',
|
||||
label: i18n.baseText(
|
||||
settingsStore.isAiGatewayCloudUbbEnabled ? 'settings.n8nCredits' : 'settings.n8nConnect',
|
||||
|
|
@ -82,6 +89,7 @@ export function useSettingsItems() {
|
|||
},
|
||||
{
|
||||
id: 'settings-roles',
|
||||
order: 60,
|
||||
icon: 'user-round',
|
||||
label: i18n.baseText('settings.roles'),
|
||||
position: 'top',
|
||||
|
|
@ -91,6 +99,7 @@ export function useSettingsItems() {
|
|||
},
|
||||
{
|
||||
id: 'settings-api',
|
||||
order: 70,
|
||||
icon: 'plug',
|
||||
label: i18n.baseText('settings.n8napi'),
|
||||
position: 'top',
|
||||
|
|
@ -99,6 +108,7 @@ export function useSettingsItems() {
|
|||
},
|
||||
{
|
||||
id: 'settings-external-secrets',
|
||||
order: 80,
|
||||
icon: 'vault',
|
||||
label: i18n.baseText('settings.externalSecrets.title'),
|
||||
position: 'top',
|
||||
|
|
@ -107,6 +117,7 @@ export function useSettingsItems() {
|
|||
},
|
||||
{
|
||||
id: 'settings-credential-resolvers',
|
||||
order: 90,
|
||||
icon: 'key-round',
|
||||
label: i18n.baseText('credentialResolver.view.title'),
|
||||
position: 'top',
|
||||
|
|
@ -115,6 +126,7 @@ export function useSettingsItems() {
|
|||
},
|
||||
{
|
||||
id: 'settings-source-control',
|
||||
order: 100,
|
||||
icon: 'git-branch',
|
||||
label: i18n.baseText('settings.sourceControl.title'),
|
||||
position: 'top',
|
||||
|
|
@ -123,6 +135,7 @@ export function useSettingsItems() {
|
|||
},
|
||||
{
|
||||
id: 'settings-promotions',
|
||||
order: 110,
|
||||
icon: 'git-branch',
|
||||
label: i18n.baseText('settings.promotions.title'),
|
||||
position: 'top',
|
||||
|
|
@ -132,6 +145,7 @@ export function useSettingsItems() {
|
|||
},
|
||||
{
|
||||
id: 'settings-sso',
|
||||
order: 120,
|
||||
icon: 'user-lock',
|
||||
label: i18n.baseText('settings.sso'),
|
||||
position: 'top',
|
||||
|
|
@ -140,6 +154,7 @@ export function useSettingsItems() {
|
|||
},
|
||||
{
|
||||
id: 'settings-encryption-keys',
|
||||
order: 130,
|
||||
icon: 'key-round',
|
||||
label: i18n.baseText('settings.encryptionKeys'),
|
||||
position: 'top',
|
||||
|
|
@ -150,6 +165,7 @@ export function useSettingsItems() {
|
|||
},
|
||||
{
|
||||
id: 'settings-security',
|
||||
order: 140,
|
||||
icon: 'shield',
|
||||
label: i18n.baseText('settings.security'),
|
||||
position: 'top',
|
||||
|
|
@ -158,6 +174,7 @@ export function useSettingsItems() {
|
|||
},
|
||||
{
|
||||
id: 'settings-ldap',
|
||||
order: 150,
|
||||
icon: 'network',
|
||||
label: i18n.baseText('settings.ldap'),
|
||||
position: 'top',
|
||||
|
|
@ -166,6 +183,7 @@ export function useSettingsItems() {
|
|||
},
|
||||
{
|
||||
id: 'settings-workersview',
|
||||
order: 160,
|
||||
icon: 'waypoints',
|
||||
label: i18n.baseText('mainSidebar.workersView'),
|
||||
position: 'top',
|
||||
|
|
@ -174,57 +192,55 @@ export function useSettingsItems() {
|
|||
hasPermission(['rbac'], { rbac: { scope: 'workersView:manage' } }),
|
||||
route: { to: { name: VIEWS.WORKER_VIEW } },
|
||||
},
|
||||
];
|
||||
|
||||
menuItems.push({
|
||||
id: 'settings-log-streaming',
|
||||
icon: 'log-in',
|
||||
label: i18n.baseText('settings.log-streaming'),
|
||||
position: 'top',
|
||||
available: canUserAccessRouteByName(VIEWS.LOG_STREAMING_SETTINGS),
|
||||
route: { to: { name: VIEWS.LOG_STREAMING_SETTINGS } },
|
||||
});
|
||||
|
||||
menuItems.push({
|
||||
id: 'settings-community-nodes',
|
||||
icon: 'box',
|
||||
label: i18n.baseText('settings.communityNodes'),
|
||||
position: 'top',
|
||||
available: canUserAccessRouteByName(VIEWS.COMMUNITY_NODES),
|
||||
route: { to: { name: VIEWS.COMMUNITY_NODES } },
|
||||
});
|
||||
|
||||
if (MIGRATION_REPORT_TARGET_VERSION) {
|
||||
menuItems.push({
|
||||
{
|
||||
id: 'settings-log-streaming',
|
||||
order: 170,
|
||||
icon: 'log-in',
|
||||
label: i18n.baseText('settings.log-streaming'),
|
||||
position: 'top',
|
||||
available: canUserAccessRouteByName(VIEWS.LOG_STREAMING_SETTINGS),
|
||||
route: { to: { name: VIEWS.LOG_STREAMING_SETTINGS } },
|
||||
},
|
||||
{
|
||||
id: 'settings-community-nodes',
|
||||
order: 180,
|
||||
icon: 'box',
|
||||
label: i18n.baseText('settings.communityNodes'),
|
||||
position: 'top',
|
||||
available: canUserAccessRouteByName(VIEWS.COMMUNITY_NODES),
|
||||
route: { to: { name: VIEWS.COMMUNITY_NODES } },
|
||||
},
|
||||
{
|
||||
id: 'settings-migration-report',
|
||||
order: 190,
|
||||
icon: 'list-checks',
|
||||
label: i18n.baseText('settings.migrationReport'),
|
||||
position: 'top',
|
||||
available: canUserAccessRouteByName(VIEWS.MIGRATION_REPORT),
|
||||
available:
|
||||
!!MIGRATION_REPORT_TARGET_VERSION && canUserAccessRouteByName(VIEWS.MIGRATION_REPORT),
|
||||
route: { to: { name: VIEWS.MIGRATION_REPORT } },
|
||||
});
|
||||
}
|
||||
},
|
||||
{
|
||||
// The flag is read here because the middleware check does not run route guards.
|
||||
id: 'settings-context',
|
||||
order: 210,
|
||||
icon: 'brain',
|
||||
label: i18n.baseText('settings.context.title'),
|
||||
position: 'top',
|
||||
available:
|
||||
isContextPreferencesEnabled() && canUserAccessRouteByName(VIEWS.SETTINGS_CONTEXT),
|
||||
route: { to: { name: VIEWS.SETTINGS_CONTEXT } },
|
||||
preview: true,
|
||||
},
|
||||
];
|
||||
|
||||
// Append module-registered settings sidebar items.
|
||||
const moduleItems = uiStore.settingsSidebarItems;
|
||||
const items = menuItems.concat(
|
||||
moduleItems.filter((item) => !menuItems.some((m) => m.id === item.id)),
|
||||
const moduleItems = uiStore.settingsSidebarItems.filter(
|
||||
(item) => !shellItems.some((shellItem) => shellItem.id === item.id),
|
||||
);
|
||||
|
||||
// After Instance-level MCP, which the MCP module appends late. The flag is read here
|
||||
// because the middleware check does not run route guards.
|
||||
const mcpIndex = items.findIndex((item) => item.id === 'settings-mcp');
|
||||
items.splice(mcpIndex === -1 ? items.length : mcpIndex + 1, 0, {
|
||||
id: 'settings-context',
|
||||
icon: 'brain',
|
||||
label: i18n.baseText('settings.context.title'),
|
||||
position: 'top',
|
||||
available: isContextPreferencesEnabled() && canUserAccessRouteByName(VIEWS.SETTINGS_CONTEXT),
|
||||
route: { to: { name: VIEWS.SETTINGS_CONTEXT } },
|
||||
preview: true,
|
||||
});
|
||||
|
||||
return items;
|
||||
return [...shellItems, ...moduleItems].sort(
|
||||
(a, b) => (a.order ?? Number.MAX_SAFE_INTEGER) - (b.order ?? Number.MAX_SAFE_INTEGER),
|
||||
);
|
||||
});
|
||||
|
||||
const visibleSettingsItems = computed(() => settingsItems.value.filter((item) => item.available));
|
||||
|
|
|
|||
|
|
@ -28,13 +28,12 @@ import { useSettingsStore } from '@n8n/stores/settings.store';
|
|||
import { applyThemeToBody, getThemeOverride, isValidTheme } from './ui.utils';
|
||||
import { SHELL_MODAL_INITIAL_STATE } from './defaults/modals';
|
||||
import { computed, ref, watch } from 'vue';
|
||||
import type { IMenuItem } from '@n8n/design-system';
|
||||
import type { Connection } from '@vue-flow/core';
|
||||
import { useLocalStorage, useMediaQuery } from '@vueuse/core';
|
||||
import type { EventBus } from '@n8n/utils/event-bus';
|
||||
import type { ProjectSharingData } from '@/features/collaboration/projects/projects.types';
|
||||
import identity from 'lodash/identity';
|
||||
import { modalRegistry } from '@n8n/frontend-module-sdk';
|
||||
import { modalRegistry, type IMenuSettingItem } from '@n8n/frontend-module-sdk';
|
||||
import { useTelemetry } from '@n8n/composables/useTelemetry';
|
||||
|
||||
let savedTheme: ThemeOption = 'system';
|
||||
|
|
@ -173,7 +172,7 @@ export const useUIStore = defineStore(STORES.UI, () => {
|
|||
* Modules can register items and SettingsSidebar will render them
|
||||
* when the corresponding module is active.
|
||||
*/
|
||||
const registeredSettingsPages = ref<Record<string, IMenuItem[]>>({});
|
||||
const registeredSettingsPages = ref<Record<string, IMenuSettingItem[]>>({});
|
||||
|
||||
const appGridDimensions = ref<{ width: number; height: number }>({ width: 0, height: 0 });
|
||||
|
||||
|
|
@ -283,8 +282,8 @@ export const useUIStore = defineStore(STORES.UI, () => {
|
|||
|
||||
const activeModals = computed(() => modalStack.value.map((modalName) => modalName));
|
||||
|
||||
const settingsSidebarItems = computed<IMenuItem[]>(() => {
|
||||
const items: IMenuItem[] = [];
|
||||
const settingsSidebarItems = computed<IMenuSettingItem[]>(() => {
|
||||
const items: IMenuSettingItem[] = [];
|
||||
Object.entries(registeredSettingsPages.value).forEach(([moduleName, moduleItems]) => {
|
||||
if (settingsStore.isModuleActive(moduleName)) {
|
||||
items.push(...moduleItems.map((item) => ({ available: true, ...item })));
|
||||
|
|
@ -600,7 +599,7 @@ export const useUIStore = defineStore(STORES.UI, () => {
|
|||
moduleTabs.value[page][moduleName] = tabs;
|
||||
};
|
||||
|
||||
const registerSettingsPages = (moduleName: string, items: IMenuItem[]) => {
|
||||
const registerSettingsPages = (moduleName: string, items: IMenuSettingItem[]) => {
|
||||
registeredSettingsPages.value[moduleName] = items;
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -163,6 +163,7 @@ export const AgentsModule = defineFrontendModule({
|
|||
settingsPages: [
|
||||
{
|
||||
id: 'settings-agents',
|
||||
order: 240,
|
||||
icon: 'robot',
|
||||
label: i18n.baseText('settings.agents'),
|
||||
position: 'top',
|
||||
|
|
|
|||
|
|
@ -195,6 +195,7 @@ export const ChatModule = defineFrontendModule({
|
|||
settingsPages: [
|
||||
{
|
||||
id: 'settings-chat-hub',
|
||||
order: 220,
|
||||
icon: 'message-circle',
|
||||
label: i18n.baseText('settings.chatHub'),
|
||||
position: 'top',
|
||||
|
|
|
|||
|
|
@ -259,6 +259,7 @@ export const InstanceAiModule = defineFrontendModule({
|
|||
settingsPages: [
|
||||
{
|
||||
id: 'settings-instance-ai',
|
||||
order: 230,
|
||||
icon: 'sparkles',
|
||||
label: i18n.baseText('settings.n8nAgent'),
|
||||
position: 'top',
|
||||
|
|
|
|||
|
|
@ -71,6 +71,7 @@ export const MCPModule = defineFrontendModule({
|
|||
settingsPages: [
|
||||
{
|
||||
id: 'settings-mcp',
|
||||
order: 200,
|
||||
icon: 'mcp',
|
||||
get label() {
|
||||
return useI18n().baseText('settings.mcp');
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user