@n8n/frontend-module-sdk 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE_EE.md ADDED
@@ -0,0 +1,27 @@
1
+ # The n8n Enterprise License (the “Enterprise License”)
2
+
3
+ Copyright (c) 2022-present n8n GmbH.
4
+
5
+ With regard to the n8n Software:
6
+
7
+ This software and associated documentation files (the "Software") may only be used in production, if
8
+ you (and any entity that you represent) hold a valid n8n Enterprise license corresponding to your
9
+ usage. Subject to the foregoing sentence, you are free to modify this Software and publish patches
10
+ to the Software. You agree that n8n and/or its licensors (as applicable) retain all right, title and
11
+ interest in and to all such modifications and/or patches, and all such modifications and/or patches
12
+ may only be used, copied, modified, displayed, distributed, or otherwise exploited with a valid n8n
13
+ Enterprise license for the corresponding usage. Notwithstanding the foregoing, you may copy and
14
+ modify the Software for development and testing purposes, without requiring a subscription. You
15
+ agree that n8n and/or its licensors (as applicable) retain all right, title and interest in and to
16
+ all such modifications. You are not granted any other rights beyond what is expressly stated herein.
17
+ Subject to the foregoing, it is forbidden to copy, merge, publish, distribute, sublicense, and/or
18
+ sell the Software.
19
+
20
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT
21
+ NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
22
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES
23
+ OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
24
+ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
25
+
26
+ For all third party components incorporated into the n8n Software, those components are licensed
27
+ under the original license provided by the owner of the applicable component.
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "type": "module",
3
3
  "name": "@n8n/frontend-module-sdk",
4
- "version": "0.1.0",
4
+ "version": "0.2.0",
5
5
  "main": "src/index.ts",
6
6
  "import": "src/index.ts",
7
7
  "exports": {
@@ -10,7 +10,8 @@
10
10
  "dependencies": {
11
11
  "vue": "^3.5.13",
12
12
  "vue-router": "^4.5.0",
13
- "@n8n/design-system": "2.30.0"
13
+ "@n8n/design-system": "2.31.0",
14
+ "@n8n/api-types": "1.32.0"
14
15
  },
15
16
  "devDependencies": {
16
17
  "typescript": "6.0.2",
@@ -18,11 +19,20 @@
18
19
  "vite": "^8.0.2",
19
20
  "vitest": "^4.1.9",
20
21
  "vue-tsc": "^2.2.8",
21
- "@n8n/typescript-config": "1.9.0",
22
22
  "@n8n/eslint-config": "0.0.1",
23
- "@n8n/vitest-config": "1.18.0"
23
+ "@n8n/vitest-config": "1.19.0",
24
+ "@n8n/typescript-config": "1.9.0"
25
+ },
26
+ "license": "SEE LICENSE IN LICENSE.md",
27
+ "homepage": "https://n8n.io",
28
+ "author": {
29
+ "name": "Jan Oberhauser",
30
+ "email": "jan@n8n.io"
31
+ },
32
+ "repository": {
33
+ "type": "git",
34
+ "url": "git+https://github.com/n8n-io/n8n.git"
24
35
  },
25
- "license": "LicenseRef-n8n-sustainable-use",
26
36
  "scripts": {
27
37
  "clean": "rimraf .turbo",
28
38
  "typecheck": "vue-tsc --noEmit",
package/src/index.ts CHANGED
@@ -1,3 +1,6 @@
1
- export type * from './module.types';
2
- export * as modalRegistry from './modalRegistry';
3
- export * from './resourceRegistry';
1
+ export type * from './types';
2
+
3
+ export * as modalRegistry from './registries/modalRegistry';
4
+ export * from './registries/resourceRegistry';
5
+ export * as pushHandlerRegistry from './registries/pushHandlerRegistry';
6
+ export * as commandRegistry from './registries/commandRegistry';
@@ -0,0 +1,67 @@
1
+ import { describe, it, expect, beforeEach, vi } from 'vitest';
2
+
3
+ import * as commandRegistry from './commandRegistry';
4
+ import type { CommandBarEntry } from '../types/command';
5
+
6
+ describe('commandRegistry', () => {
7
+ const commandA: CommandBarEntry = { id: 'cmd-a', title: 'Command A' };
8
+ const commandB: CommandBarEntry = {
9
+ id: 'cmd-b',
10
+ title: 'Command B',
11
+ section: 'Navigation',
12
+ keywords: ['jump'],
13
+ };
14
+
15
+ beforeEach(() => {
16
+ commandRegistry.clear();
17
+ });
18
+
19
+ it('should register and retrieve a command', () => {
20
+ commandRegistry.register(commandA);
21
+
22
+ expect(commandRegistry.has('cmd-a')).toBe(true);
23
+ expect(commandRegistry.get('cmd-a')).toEqual(commandA);
24
+ });
25
+
26
+ it('should return all registered commands in registration order', () => {
27
+ commandRegistry.register(commandA);
28
+ commandRegistry.register(commandB);
29
+
30
+ expect(commandRegistry.getAll()).toEqual([commandA, commandB]);
31
+ });
32
+
33
+ it('should warn and skip when an id is registered twice', () => {
34
+ const consoleSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
35
+
36
+ commandRegistry.register(commandA);
37
+ commandRegistry.register(commandA);
38
+
39
+ expect(consoleSpy).toHaveBeenCalledWith(
40
+ 'Command with id "cmd-a" is already registered. Skipping.',
41
+ );
42
+ expect(commandRegistry.getAll()).toHaveLength(1);
43
+
44
+ consoleSpy.mockRestore();
45
+ });
46
+
47
+ it('should notify subscribers on register and unregister', () => {
48
+ const listener = vi.fn();
49
+ commandRegistry.subscribe(listener);
50
+
51
+ commandRegistry.register(commandA);
52
+ expect(listener).toHaveBeenLastCalledWith([commandA]);
53
+
54
+ commandRegistry.unregister('cmd-a');
55
+ expect(listener).toHaveBeenLastCalledWith([]);
56
+ });
57
+
58
+ it('should return an unsubscribe function that stops notifications', () => {
59
+ const listener = vi.fn();
60
+ const unsubscribe = commandRegistry.subscribe(listener);
61
+
62
+ unsubscribe();
63
+ commandRegistry.register(commandA);
64
+
65
+ expect(listener).not.toHaveBeenCalled();
66
+ });
67
+ });
@@ -0,0 +1,51 @@
1
+ import type { CommandBarEntry } from '../types/command';
2
+
3
+ const commands = new Map<string, CommandBarEntry>();
4
+ const listeners = new Set<(commands: CommandBarEntry[]) => void>();
5
+
6
+ export function getAll(): CommandBarEntry[] {
7
+ return Array.from(commands.values());
8
+ }
9
+
10
+ function notifyListeners(): void {
11
+ const snapshot = getAll();
12
+ listeners.forEach((listener) => listener(snapshot));
13
+ }
14
+
15
+ export function register(command: CommandBarEntry): void {
16
+ if (commands.has(command.id)) {
17
+ console.warn(`Command with id "${command.id}" is already registered. Skipping.`);
18
+ return;
19
+ }
20
+ commands.set(command.id, command);
21
+ notifyListeners();
22
+ }
23
+
24
+ export function unregister(id: string): void {
25
+ if (commands.delete(id)) {
26
+ notifyListeners();
27
+ }
28
+ }
29
+
30
+ export function get(id: string): CommandBarEntry | undefined {
31
+ return commands.get(id);
32
+ }
33
+
34
+ export function has(id: string): boolean {
35
+ return commands.has(id);
36
+ }
37
+
38
+ export function subscribe(listener: (commands: CommandBarEntry[]) => void): () => void {
39
+ listeners.add(listener);
40
+ return () => {
41
+ listeners.delete(listener);
42
+ };
43
+ }
44
+
45
+ /**
46
+ * Remove all registered commands. Primarily for test isolation.
47
+ */
48
+ export function clear(): void {
49
+ commands.clear();
50
+ notifyListeners();
51
+ }
@@ -2,7 +2,7 @@ import { describe, it, expect, beforeEach, vi } from 'vitest';
2
2
  import type { Component } from 'vue';
3
3
 
4
4
  import * as modalRegistry from './modalRegistry';
5
- import type { ModalDefinition } from './module.types';
5
+ import type { ModalDefinition } from '../types/modal';
6
6
 
7
7
  describe('modalRegistry', () => {
8
8
  const mockComponent1 = { name: 'TestModal1' } as Component;
@@ -1,4 +1,4 @@
1
- import type { ModalDefinition } from './module.types';
1
+ import type { ModalDefinition } from '../types/modal';
2
2
 
3
3
  const modals = new Map<string, ModalDefinition>();
4
4
  const listeners = new Set<(modals: Map<string, ModalDefinition>) => void>();
@@ -0,0 +1,75 @@
1
+ import type { PushMessage } from '@n8n/api-types';
2
+ import { describe, it, expect, beforeEach, vi } from 'vitest';
3
+
4
+ import * as pushHandlerRegistry from './pushHandlerRegistry';
5
+ import type { ModulePushHandlerContext, ModulePushHandlers } from '../types/push';
6
+
7
+ const context = { router: {} } as unknown as ModulePushHandlerContext;
8
+
9
+ describe('pushHandlerRegistry', () => {
10
+ beforeEach(() => {
11
+ pushHandlerRegistry.clear();
12
+ });
13
+
14
+ it('should register and retrieve a handler by type', () => {
15
+ const handler = vi.fn();
16
+ pushHandlerRegistry.register('executionFinished', handler);
17
+
18
+ expect(pushHandlerRegistry.has('executionFinished')).toBe(true);
19
+ expect(pushHandlerRegistry.get('executionFinished')).toBe(handler);
20
+ });
21
+
22
+ it('should return undefined for an unregistered type', () => {
23
+ expect(pushHandlerRegistry.get('executionStarted')).toBeUndefined();
24
+ expect(pushHandlerRegistry.has('executionStarted')).toBe(false);
25
+ });
26
+
27
+ it('should invoke the registered handler with the event and context', async () => {
28
+ const handler = vi.fn();
29
+ pushHandlerRegistry.register('executionStarted', handler);
30
+
31
+ const event = { type: 'executionStarted', data: {} } as unknown as PushMessage;
32
+ await pushHandlerRegistry.get('executionStarted')?.(event, context);
33
+
34
+ expect(handler).toHaveBeenCalledWith(event, context);
35
+ });
36
+
37
+ it('should warn and keep the first handler when a type is registered twice', () => {
38
+ const consoleSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
39
+ const first = vi.fn();
40
+ const second = vi.fn();
41
+
42
+ pushHandlerRegistry.register('workflowActivated', first);
43
+ pushHandlerRegistry.register('workflowActivated', second);
44
+
45
+ expect(consoleSpy).toHaveBeenCalledWith(
46
+ 'Push handler for type "workflowActivated" is already registered. Skipping.',
47
+ );
48
+ expect(pushHandlerRegistry.get('workflowActivated')).toBe(first);
49
+
50
+ consoleSpy.mockRestore();
51
+ });
52
+
53
+ it('should register every entry in a module pushHandlers map', () => {
54
+ const activated = vi.fn();
55
+ const deactivated = vi.fn();
56
+ const handlers: ModulePushHandlers = {
57
+ workflowActivated: activated,
58
+ workflowDeactivated: deactivated,
59
+ };
60
+
61
+ pushHandlerRegistry.registerAll(handlers);
62
+
63
+ expect(pushHandlerRegistry.getTypes()).toEqual(
64
+ expect.arrayContaining(['workflowActivated', 'workflowDeactivated']),
65
+ );
66
+ expect(pushHandlerRegistry.get('workflowActivated')).toBeDefined();
67
+ });
68
+
69
+ it('should unregister a handler', () => {
70
+ pushHandlerRegistry.register('workflowDeactivated', vi.fn());
71
+ pushHandlerRegistry.unregister('workflowDeactivated');
72
+
73
+ expect(pushHandlerRegistry.has('workflowDeactivated')).toBe(false);
74
+ });
75
+ });
@@ -0,0 +1,52 @@
1
+ import type { PushType } from '@n8n/api-types';
2
+
3
+ import type { ModulePushHandler, ModulePushHandlers } from '../types/push';
4
+
5
+ // One handler per push message type. The shell consults this before its own
6
+ // switch, so a module can own (or override) a push message type.
7
+ const handlers = new Map<PushType, ModulePushHandler>();
8
+
9
+ export function register(type: PushType, handler: ModulePushHandler): void {
10
+ if (handlers.has(type)) {
11
+ console.warn(`Push handler for type "${type}" is already registered. Skipping.`);
12
+ return;
13
+ }
14
+ handlers.set(type, handler);
15
+ }
16
+
17
+ /**
18
+ * Register every handler in a module's `pushHandlers` map. Centralises the one
19
+ * cast needed to erase the per-type event narrowing: each handler is stored
20
+ * against its own key, so it only ever receives an event of that type.
21
+ */
22
+ export function registerAll(pushHandlers: ModulePushHandlers): void {
23
+ for (const type of Object.keys(pushHandlers) as PushType[]) {
24
+ const handler = pushHandlers[type];
25
+ if (handler) {
26
+ register(type, handler as ModulePushHandler);
27
+ }
28
+ }
29
+ }
30
+
31
+ export function get(type: PushType): ModulePushHandler | undefined {
32
+ return handlers.get(type);
33
+ }
34
+
35
+ export function has(type: PushType): boolean {
36
+ return handlers.has(type);
37
+ }
38
+
39
+ export function getTypes(): PushType[] {
40
+ return Array.from(handlers.keys());
41
+ }
42
+
43
+ export function unregister(type: PushType): void {
44
+ handlers.delete(type);
45
+ }
46
+
47
+ /**
48
+ * Remove all registered handlers. Primarily for test isolation.
49
+ */
50
+ export function clear(): void {
51
+ handlers.clear();
52
+ }
@@ -1,6 +1,6 @@
1
1
  import { describe, it, expect, beforeEach, vi } from 'vitest';
2
2
 
3
- import type { ResourceMetadata } from './module.types';
3
+ import type { ResourceMetadata } from '../types/resource';
4
4
 
5
5
  // The registry keeps module-level state and exposes no reset, so re-import a
6
6
  // fresh module instance before each test to keep them isolated.
@@ -14,7 +14,7 @@
14
14
  4. Import your resource type from the local types file in your components
15
15
  */
16
16
 
17
- import { type ResourceMetadata } from './module.types';
17
+ import { type ResourceMetadata } from '../types/resource';
18
18
 
19
19
  // Private module state
20
20
  const resources: Map<string, ResourceMetadata> = new Map();
@@ -0,0 +1,12 @@
1
+ import type { Component } from 'vue';
2
+
3
+ /**
4
+ * A banner a module contributes to the shell's banner stack. `priority` follows
5
+ * the existing stack semantics (higher wins when several banners are queued).
6
+ */
7
+ export interface ModuleBanner {
8
+ name: string;
9
+ priority: number;
10
+ component: Component | (() => Promise<Component>);
11
+ dismissible?: boolean;
12
+ }
@@ -0,0 +1,18 @@
1
+ /**
2
+ * A command-bar entry a module contributes. Deliberately self-contained (a
3
+ * stable subset of the design-system `CommandBarItem`) so the module contract
4
+ * does not couple to command-bar internals.
5
+ */
6
+ export interface CommandBarEntry {
7
+ id: string;
8
+ title: string;
9
+ section?: string;
10
+ keywords?: string[];
11
+ icon?: string;
12
+ handler?: () => void | Promise<void>;
13
+ children?: CommandBarEntry[];
14
+ /**
15
+ * Route names this command is active on. Undefined/empty means all views.
16
+ */
17
+ activeViews?: string[];
18
+ }
@@ -0,0 +1,46 @@
1
+ import { describe, it, expect } from 'vitest';
2
+
3
+ import type { FrontendModuleDescription } from './descriptor';
4
+
5
+ describe('FrontendModuleDescription', () => {
6
+ it('accepts a v1-shaped descriptor (no v2 fields)', () => {
7
+ const descriptor: FrontendModuleDescription = {
8
+ id: 'legacy',
9
+ name: 'Legacy',
10
+ description: 'A descriptor using only the original fields',
11
+ icon: 'box',
12
+ routes: [],
13
+ resources: [{ key: 'legacy', displayName: 'Legacy' }],
14
+ modals: [],
15
+ settingsPages: [],
16
+ };
17
+
18
+ expect(descriptor.id).toBe('legacy');
19
+ });
20
+
21
+ it('accepts a v2 descriptor exercising the new optional fields', () => {
22
+ const cleanups: Array<() => void> = [];
23
+ const noop = () => {};
24
+ const descriptor: FrontendModuleDescription = {
25
+ id: 'v2',
26
+ name: 'V2',
27
+ description: 'A descriptor using the v2 fields',
28
+ icon: 'box',
29
+ locales: { en: { greeting: 'hi' } },
30
+ pushHandlers: {
31
+ workflowActivated: async () => {},
32
+ },
33
+ commands: [{ id: 'v2.open', title: 'Open V2' }],
34
+ shortcuts: [{ keys: 'ctrl+shift+v', run: () => {} }],
35
+ banners: [{ name: 'v2-banner', priority: 10, component: { name: 'Banner' } }],
36
+ setup: (ctx) => {
37
+ ctx.registerCleanup(() => {
38
+ cleanups.push(noop);
39
+ });
40
+ },
41
+ };
42
+
43
+ expect(descriptor.commands?.[0]?.id).toBe('v2.open');
44
+ expect(descriptor.pushHandlers?.workflowActivated).toBeTypeOf('function');
45
+ });
46
+ });
@@ -0,0 +1,50 @@
1
+ import type { IMenuItem } from '@n8n/design-system';
2
+ import type { RouteRecordRaw } from 'vue-router';
3
+
4
+ import type { ModuleBanner } from './banner';
5
+ import type { CommandBarEntry } from './command';
6
+ import type { ModuleLocaleMessages } from './locale';
7
+ import type { ModalDefinition } from './modal';
8
+ import type { ModulePushHandlers } from './push';
9
+ import type { ResourceMetadata } from './resource';
10
+ import type { ModuleSetupContext } from './setup';
11
+ import type { ModuleShortcut } from './shortcut';
12
+ import type { DynamicTabOptions } from './tabs';
13
+
14
+ /**
15
+ * The declarative contract a frontend module exposes to the editor-ui shell.
16
+ *
17
+ * Descriptor v2 adds `locales`, `pushHandlers`, `commands`, `shortcuts`,
18
+ * `banners`, and a post-login `setup(ctx)` hook. Every v2 field is optional and
19
+ * additive, so existing descriptors satisfy the type unchanged.
20
+ */
21
+ export type FrontendModuleDescription = {
22
+ id: string;
23
+ name: string;
24
+ description: string;
25
+ icon: string;
26
+ routes?: RouteRecordRaw[];
27
+ projectTabs?: {
28
+ overview?: DynamicTabOptions[];
29
+ project?: DynamicTabOptions[];
30
+ shared?: DynamicTabOptions[];
31
+ };
32
+ resources?: ResourceMetadata[];
33
+ modals?: ModalDefinition[];
34
+ settingsPages?: IMenuItem[];
35
+
36
+ // --- descriptor v2 (all optional, additive) ---
37
+
38
+ /** Per-module i18n messages, merged into the active locale by the shell. */
39
+ locales?: ModuleLocaleMessages;
40
+ /** Push-message handlers, keyed by message type. */
41
+ pushHandlers?: ModulePushHandlers;
42
+ /** Command-bar contributions. */
43
+ commands?: CommandBarEntry[];
44
+ /** Global keyboard shortcuts. */
45
+ shortcuts?: ModuleShortcut[];
46
+ /** Banners the module can contribute to the banner stack. */
47
+ banners?: ModuleBanner[];
48
+ /** Runs post-login, after the module is confirmed active. */
49
+ setup?: (ctx: ModuleSetupContext) => void | Promise<void>;
50
+ };
@@ -0,0 +1,10 @@
1
+ export type * from './descriptor';
2
+ export type * from './modal';
3
+ export type * from './resource';
4
+ export type * from './tabs';
5
+ export type * from './push';
6
+ export type * from './command';
7
+ export type * from './shortcut';
8
+ export type * from './banner';
9
+ export type * from './setup';
10
+ export type * from './locale';
@@ -0,0 +1,5 @@
1
+ /**
2
+ * Per-module i18n messages, keyed by locale code (e.g. `en`). Values are merged
3
+ * additively into the active vue-i18n bundle by the shell.
4
+ */
5
+ export type ModuleLocaleMessages = Record<string, Record<string, unknown>>;
@@ -0,0 +1,20 @@
1
+ import type { Component } from 'vue';
2
+
3
+ /**
4
+ * Shared state shape a modal is initialized with. The SDK owns this type; the
5
+ * editor-ui shell re-exports it from `@/Interface` for backwards compatibility.
6
+ */
7
+ export type ModalState = {
8
+ open: boolean;
9
+ mode?: string | null;
10
+ data?: Record<string, unknown>;
11
+ activeId?: string | null;
12
+ curlCommand?: string;
13
+ httpNodeParameters?: string;
14
+ };
15
+
16
+ export type ModalDefinition = {
17
+ key: string;
18
+ component: Component | (() => Promise<Component>);
19
+ initialState?: ModalState;
20
+ };
@@ -0,0 +1,30 @@
1
+ import type { PushMessage, PushType } from '@n8n/api-types';
2
+ import type { Router } from 'vue-router';
3
+
4
+ /**
5
+ * Context handed to a module push handler when it runs. The shell may pass a
6
+ * richer context (it extends this); handlers only see the stable surface.
7
+ */
8
+ export interface ModulePushHandlerContext {
9
+ router: Router;
10
+ }
11
+
12
+ /**
13
+ * Handler for a single push message type. Contravariant in its context so the
14
+ * shell can supply a superset context at call time.
15
+ */
16
+ export type ModulePushHandler<Ctx extends ModulePushHandlerContext = ModulePushHandlerContext> = (
17
+ event: PushMessage,
18
+ context: Ctx,
19
+ ) => void | Promise<void>;
20
+
21
+ /**
22
+ * A module's push-message contributions, keyed by message type. Each handler
23
+ * receives the event already narrowed to its type.
24
+ */
25
+ export type ModulePushHandlers = {
26
+ [T in PushType]?: (
27
+ event: Extract<PushMessage, { type: T }>,
28
+ context: ModulePushHandlerContext,
29
+ ) => void | Promise<void>;
30
+ };
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Metadata a module contributes to describe a resource type it owns. Consumed by
3
+ * generic list surfaces (e.g. `ResourcesListLayout`).
4
+ */
5
+ export type ResourceMetadata = {
6
+ key: string;
7
+ displayName: string;
8
+ i18nKeys?: Record<string, string>;
9
+ };
@@ -0,0 +1,16 @@
1
+ import type { Router } from 'vue-router';
2
+
3
+ export type ModuleCleanupFn = () => void | Promise<void>;
4
+
5
+ /**
6
+ * Context handed to a module's post-login `setup(ctx)` hook. Kept intentionally
7
+ * small; grow it as modules need more, rather than exposing stores directly.
8
+ */
9
+ export interface ModuleSetupContext {
10
+ router: Router;
11
+ /**
12
+ * Register a teardown callback to run when the authenticated session ends
13
+ * (e.g. on logout). Cleanups run in reverse registration order.
14
+ */
15
+ registerCleanup: (fn: ModuleCleanupFn) => void;
16
+ }
@@ -0,0 +1,9 @@
1
+ /**
2
+ * A global keyboard shortcut a module contributes. `keys` uses the same syntax
3
+ * as the shell's `useKeybindings` (e.g. `'ctrl+k'`, `'ctrl+b|ctrl+c'`).
4
+ */
5
+ export interface ModuleShortcut {
6
+ keys: string;
7
+ run: (event: KeyboardEvent) => void;
8
+ disabled?: () => boolean;
9
+ }
@@ -0,0 +1,17 @@
1
+ import type { TabOptions } from '@n8n/design-system';
2
+
3
+ /**
4
+ * A project/overview tab a module contributes. `dynamicRoute` is resolved with
5
+ * the current project id at render time by the shell's `processDynamicTab`.
6
+ */
7
+ export type DynamicTabOptions = TabOptions<string> & {
8
+ dynamicRoute?: {
9
+ name: string;
10
+ includeProjectId?: boolean;
11
+ };
12
+ /**
13
+ * Insert this tab immediately after the tab whose `value` matches.
14
+ * If unset (or no match is found at render time), the tab is appended at the end.
15
+ */
16
+ insertAfter?: string;
17
+ };
@@ -1,52 +0,0 @@
1
- import type { IMenuItem, TabOptions } from '@n8n/design-system';
2
- import type { Component } from 'vue';
3
- import type { RouteRecordRaw } from 'vue-router';
4
-
5
- export type ModalState = {
6
- open: boolean;
7
- mode?: string | null;
8
- data?: Record<string, unknown>;
9
- activeId?: string | null;
10
- curlCommand?: string;
11
- httpNodeParameters?: string;
12
- };
13
-
14
- export type DynamicTabOptions = TabOptions<string> & {
15
- dynamicRoute?: {
16
- name: string;
17
- includeProjectId?: boolean;
18
- };
19
- /**
20
- * Insert this tab immediately after the tab whose `value` matches.
21
- * If unset (or no match is found at render time), the tab is appended at the end.
22
- */
23
- insertAfter?: string;
24
- };
25
-
26
- export type ModalDefinition = {
27
- key: string;
28
- component: Component | (() => Promise<Component>);
29
- initialState?: ModalState;
30
- };
31
-
32
- export type ResourceMetadata = {
33
- key: string;
34
- displayName: string;
35
- i18nKeys?: Record<string, string>;
36
- };
37
-
38
- export type FrontendModuleDescription = {
39
- id: string;
40
- name: string;
41
- description: string;
42
- icon: string;
43
- routes?: RouteRecordRaw[];
44
- projectTabs?: {
45
- overview?: DynamicTabOptions[];
46
- project?: DynamicTabOptions[];
47
- shared?: DynamicTabOptions[];
48
- };
49
- resources?: ResourceMetadata[];
50
- modals?: ModalDefinition[];
51
- settingsPages?: IMenuItem[];
52
- };