From 3a65bdc1f522932d463b4da0e67d29076887d06c Mon Sep 17 00:00:00 2001 From: oleg Date: Mon, 30 Sep 2024 10:09:39 +0200 Subject: [PATCH 001/304] fix(AI Agent Node): Fix output parsing and empty tool input handling in AI Agent node (#10970) --- .../agents/Agent/agents/ToolsAgent/execute.ts | 87 ++++++++++++++----- 1 file changed, 65 insertions(+), 22 deletions(-) diff --git a/packages/@n8n/nodes-langchain/nodes/agents/Agent/agents/ToolsAgent/execute.ts b/packages/@n8n/nodes-langchain/nodes/agents/Agent/agents/ToolsAgent/execute.ts index a6dc4a63f2..90952bac41 100644 --- a/packages/@n8n/nodes-langchain/nodes/agents/Agent/agents/ToolsAgent/execute.ts +++ b/packages/@n8n/nodes-langchain/nodes/agents/Agent/agents/ToolsAgent/execute.ts @@ -1,27 +1,28 @@ -import { BINARY_ENCODING, NodeConnectionType, NodeOperationError } from 'n8n-workflow'; -import type { IExecuteFunctions, INodeExecutionData } from 'n8n-workflow'; - -import type { AgentAction, AgentFinish } from 'langchain/agents'; -import { AgentExecutor, createToolCallingAgent } from 'langchain/agents'; import type { BaseChatMemory } from '@langchain/community/memory/chat_memory'; +import { HumanMessage } from '@langchain/core/messages'; +import type { BaseMessage } from '@langchain/core/messages'; +import type { BaseOutputParser, StructuredOutputParser } from '@langchain/core/output_parsers'; import type { BaseMessagePromptTemplateLike } from '@langchain/core/prompts'; import { ChatPromptTemplate } from '@langchain/core/prompts'; -import { omit } from 'lodash'; +import { RunnableSequence } from '@langchain/core/runnables'; import type { Tool } from '@langchain/core/tools'; import { DynamicStructuredTool } from '@langchain/core/tools'; +import type { AgentAction, AgentFinish } from 'langchain/agents'; +import { AgentExecutor, createToolCallingAgent } from 'langchain/agents'; +import { OutputFixingParser } from 'langchain/output_parsers'; +import { omit } from 'lodash'; +import { BINARY_ENCODING, jsonParse, NodeConnectionType, NodeOperationError } from 'n8n-workflow'; +import type { IExecuteFunctions, INodeExecutionData } from 'n8n-workflow'; import type { ZodObject } from 'zod'; import { z } from 'zod'; -import type { BaseOutputParser, StructuredOutputParser } from '@langchain/core/output_parsers'; -import { OutputFixingParser } from 'langchain/output_parsers'; -import { HumanMessage } from '@langchain/core/messages'; -import { RunnableSequence } from '@langchain/core/runnables'; + +import { SYSTEM_MESSAGE } from './prompt'; import { isChatInstance, getPromptInputByType, getOptionalOutputParsers, getConnectedTools, } from '../../../../../utils/helpers'; -import { SYSTEM_MESSAGE } from './prompt'; function getOutputParserSchema(outputParser: BaseOutputParser): ZodObject { const parserType = outputParser.lc_namespace[outputParser.lc_namespace.length - 1]; @@ -74,6 +75,39 @@ async function extractBinaryMessages(ctx: IExecuteFunctions) { content: [...binaryMessages], }); } +/** + * Fixes empty content messages in agent steps. + * + * This function is necessary when using RunnableSequence.from in LangChain. + * If a tool doesn't have any arguments, LangChain returns input: '' (empty string). + * This can throw an error for some providers (like Anthropic) which expect the input to always be an object. + * This function replaces empty string inputs with empty objects to prevent such errors. + * + * @param steps - The agent steps to fix + * @returns The fixed agent steps + */ +function fixEmptyContentMessage(steps: AgentFinish | AgentAction[]) { + if (!Array.isArray(steps)) return steps; + + steps.forEach((step) => { + if ('messageLog' in step && step.messageLog !== undefined) { + if (Array.isArray(step.messageLog)) { + step.messageLog.forEach((message: BaseMessage) => { + if ('content' in message && Array.isArray(message.content)) { + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access + (message.content as Array<{ input?: string | object }>).forEach((content) => { + if (content.input === '') { + content.input = {}; + } + }); + } + }); + } + } + }); + + return steps; +} export async function toolsAgentExecute(this: IExecuteFunctions): Promise { this.logger.debug('Executing Tools Agent'); @@ -156,6 +190,14 @@ export async function toolsAgentExecute(this: IExecuteFunctions): Promise) { + return { + returnValues: memory ? { output: JSON.stringify(output) } : output, + log: 'Final response formatted', + }; + } async function agentStepsParser( steps: AgentFinish | AgentAction[], ): Promise { @@ -168,24 +210,18 @@ export async function toolsAgentExecute(this: IExecuteFunctions): Promise; - return { - returnValues, - log: 'Final response formatted', - }; + return handleParsedStepOutput(returnValues); } } - // If the steps are an AgentFinish and the outputParser is defined it must mean that the LLM didn't use `format_final_response` tool so we will parse the output manually + + // If the steps are an AgentFinish and the outputParser is defined it must mean that the LLM didn't use `format_final_response` tool so we will try to parse the output manually if (outputParser && typeof steps === 'object' && (steps as AgentFinish).returnValues) { const finalResponse = (steps as AgentFinish).returnValues; const returnValues = (await outputParser.parse(finalResponse as unknown as string)) as Record< string, unknown >; - - return { - returnValues, - log: 'Final response formatted', - }; + return handleParsedStepOutput(returnValues); } return handleAgentFinishOutput(steps); } @@ -233,7 +269,7 @@ export async function toolsAgentExecute(this: IExecuteFunctions): Promise }>( + response.output as string, + ); + response.output = parsedOutput?.output ?? parsedOutput; + } + returnData.push({ json: omit( response, From 63e6f1fa38c7085f7246ae1c94bd47b2e066c812 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Iv=C3=A1n=20Ovejero?= Date: Mon, 30 Sep 2024 10:44:03 +0200 Subject: [PATCH 002/304] refactor(core): Organize all event maps (#10997) --- packages/cli/src/commands/base-command.ts | 2 +- packages/cli/src/commands/worker.ts | 2 +- packages/cli/src/decorators/redactable.ts | 2 +- .../log-streaming-event-relay.test.ts | 4 +- .../__tests__/telemetry-event-relay.test.ts | 4 +- packages/cli/src/events/event.service.ts | 9 +- .../{ai-event-map.ts => maps/ai.event-map.ts} | 0 .../cli/src/events/maps/pub-sub.event-map.ts | 104 +++++++++++ .../queue-metrics.event-map.ts} | 0 .../relay.event-map.ts} | 2 +- .../src/events/{ => relays}/event-relay.ts | 5 +- .../log-streaming.event-relay.ts} | 7 +- .../telemetry.event-relay.ts} | 4 +- .../cli/src/scaling/pubsub/pubsub.types.ts | 161 ++++-------------- packages/cli/src/server.ts | 2 +- .../cli/src/services/orchestration.service.ts | 6 +- .../src/workflow-execute-additional-data.ts | 2 +- .../integration/commands/worker.cmd.test.ts | 2 +- .../integration/shared/utils/test-command.ts | 2 +- 19 files changed, 161 insertions(+), 159 deletions(-) rename packages/cli/src/events/{ai-event-map.ts => maps/ai.event-map.ts} (100%) create mode 100644 packages/cli/src/events/maps/pub-sub.event-map.ts rename packages/cli/src/events/{queue-metrics-event-map.ts => maps/queue-metrics.event-map.ts} (100%) rename packages/cli/src/events/{relay-event-map.ts => maps/relay.event-map.ts} (99%) rename packages/cli/src/events/{ => relays}/event-relay.ts (81%) rename packages/cli/src/events/{log-streaming-event-relay.ts => relays/log-streaming.event-relay.ts} (98%) rename packages/cli/src/events/{telemetry-event-relay.ts => relays/telemetry.event-relay.ts} (99%) diff --git a/packages/cli/src/commands/base-command.ts b/packages/cli/src/commands/base-command.ts index 857ca231d4..109d6a91f5 100644 --- a/packages/cli/src/commands/base-command.ts +++ b/packages/cli/src/commands/base-command.ts @@ -13,7 +13,7 @@ import { generateHostInstanceId } from '@/databases/utils/generators'; import * as Db from '@/db'; import { initErrorHandling } from '@/error-reporting'; import { MessageEventBus } from '@/eventbus/message-event-bus/message-event-bus'; -import { TelemetryEventRelay } from '@/events/telemetry-event-relay'; +import { TelemetryEventRelay } from '@/events/relays/telemetry.event-relay'; import { initExpressionEvaluator } from '@/expression-evaluator'; import { ExternalHooks } from '@/external-hooks'; import { ExternalSecretsManager } from '@/external-secrets/external-secrets-manager.ee'; diff --git a/packages/cli/src/commands/worker.ts b/packages/cli/src/commands/worker.ts index f5f6b2b79b..8c1aabf74a 100644 --- a/packages/cli/src/commands/worker.ts +++ b/packages/cli/src/commands/worker.ts @@ -6,7 +6,7 @@ import config from '@/config'; import { N8N_VERSION, inTest } from '@/constants'; import { EventMessageGeneric } from '@/eventbus/event-message-classes/event-message-generic'; import { MessageEventBus } from '@/eventbus/message-event-bus/message-event-bus'; -import { LogStreamingEventRelay } from '@/events/log-streaming-event-relay'; +import { LogStreamingEventRelay } from '@/events/relays/log-streaming.event-relay'; import { JobProcessor } from '@/scaling/job-processor'; import { Publisher } from '@/scaling/pubsub/publisher.service'; import type { ScalingService } from '@/scaling/scaling.service'; diff --git a/packages/cli/src/decorators/redactable.ts b/packages/cli/src/decorators/redactable.ts index 51d02c5c3d..e2df19daa6 100644 --- a/packages/cli/src/decorators/redactable.ts +++ b/packages/cli/src/decorators/redactable.ts @@ -1,5 +1,5 @@ import { RedactableError } from '@/errors/redactable.error'; -import type { UserLike } from '@/events/relay-event-map'; +import type { UserLike } from '@/events/maps/relay.event-map'; function toRedactable(userLike: UserLike) { return { diff --git a/packages/cli/src/events/__tests__/log-streaming-event-relay.test.ts b/packages/cli/src/events/__tests__/log-streaming-event-relay.test.ts index d768218950..4727c8ef72 100644 --- a/packages/cli/src/events/__tests__/log-streaming-event-relay.test.ts +++ b/packages/cli/src/events/__tests__/log-streaming-event-relay.test.ts @@ -3,8 +3,8 @@ import type { INode, IRun, IWorkflowBase } from 'n8n-workflow'; import type { MessageEventBus } from '@/eventbus/message-event-bus/message-event-bus'; import { EventService } from '@/events/event.service'; -import { LogStreamingEventRelay } from '@/events/log-streaming-event-relay'; -import type { RelayEventMap } from '@/events/relay-event-map'; +import type { RelayEventMap } from '@/events/maps/relay.event-map'; +import { LogStreamingEventRelay } from '@/events/relays/log-streaming.event-relay'; import type { IWorkflowDb } from '@/interfaces'; describe('LogStreamingEventRelay', () => { diff --git a/packages/cli/src/events/__tests__/telemetry-event-relay.test.ts b/packages/cli/src/events/__tests__/telemetry-event-relay.test.ts index 9a05835205..124afd901b 100644 --- a/packages/cli/src/events/__tests__/telemetry-event-relay.test.ts +++ b/packages/cli/src/events/__tests__/telemetry-event-relay.test.ts @@ -9,8 +9,8 @@ import type { ProjectRelationRepository } from '@/databases/repositories/project import type { SharedWorkflowRepository } from '@/databases/repositories/shared-workflow.repository'; import type { WorkflowRepository } from '@/databases/repositories/workflow.repository'; import { EventService } from '@/events/event.service'; -import type { RelayEventMap } from '@/events/relay-event-map'; -import { TelemetryEventRelay } from '@/events/telemetry-event-relay'; +import type { RelayEventMap } from '@/events/maps/relay.event-map'; +import { TelemetryEventRelay } from '@/events/relays/telemetry.event-relay'; import type { IWorkflowDb } from '@/interfaces'; import type { License } from '@/license'; import type { NodeTypes } from '@/node-types'; diff --git a/packages/cli/src/events/event.service.ts b/packages/cli/src/events/event.service.ts index 10ba7666ef..b8e00ecea7 100644 --- a/packages/cli/src/events/event.service.ts +++ b/packages/cli/src/events/event.service.ts @@ -2,11 +2,12 @@ import { Service } from 'typedi'; import { TypedEmitter } from '@/typed-emitter'; -import type { AiEventMap } from './ai-event-map'; -import type { QueueMetricsEventMap } from './queue-metrics-event-map'; -import type { RelayEventMap } from './relay-event-map'; +import type { AiEventMap } from './maps/ai.event-map'; +import type { PubSubEventMap } from './maps/pub-sub.event-map'; +import type { QueueMetricsEventMap } from './maps/queue-metrics.event-map'; +import type { RelayEventMap } from './maps/relay.event-map'; -type EventMap = RelayEventMap & QueueMetricsEventMap & AiEventMap; +type EventMap = RelayEventMap & QueueMetricsEventMap & AiEventMap & PubSubEventMap; @Service() export class EventService extends TypedEmitter {} diff --git a/packages/cli/src/events/ai-event-map.ts b/packages/cli/src/events/maps/ai.event-map.ts similarity index 100% rename from packages/cli/src/events/ai-event-map.ts rename to packages/cli/src/events/maps/ai.event-map.ts diff --git a/packages/cli/src/events/maps/pub-sub.event-map.ts b/packages/cli/src/events/maps/pub-sub.event-map.ts new file mode 100644 index 0000000000..9237e79d13 --- /dev/null +++ b/packages/cli/src/events/maps/pub-sub.event-map.ts @@ -0,0 +1,104 @@ +import type { WorkerStatus, PushType } from '@n8n/api-types'; + +import type { IWorkflowDb } from '@/interfaces'; + +export type PubSubEventMap = PubSubCommandMap & PubSubWorkerResponseMap; + +export type PubSubCommandMap = { + // #region Lifecycle + + 'reload-license': never; + + 'restart-event-bus': never; + + 'reload-external-secrets-providers': never; + + // #endregion + + // #region Community packages + + 'community-package-install': { + packageName: string; + packageVersion: string; + }; + + 'community-package-update': { + packageName: string; + packageVersion: string; + }; + + 'community-package-uninstall': { + packageName: string; + }; + + // #endregion + + // #region Worker view + + 'get-worker-id': never; + + 'get-worker-status': never; + + // #endregion + + // #region Multi-main setup + + 'add-webhooks-triggers-and-pollers': { + workflowId: string; + }; + + 'remove-triggers-and-pollers': { + workflowId: string; + }; + + 'display-workflow-activation': { + workflowId: string; + }; + + 'display-workflow-deactivation': { + workflowId: string; + }; + + 'display-workflow-activation-error': { + workflowId: string; + errorMessage: string; + }; + + 'relay-execution-lifecycle-event': { + type: PushType; + args: Record; + pushRef: string; + }; + + 'clear-test-webhooks': { + webhookKey: string; + workflowEntity: IWorkflowDb; + pushRef: string; + }; + + // #endregion +}; + +export type PubSubWorkerResponseMap = { + // #region Lifecycle + + 'restart-event-bus': { + result: 'success' | 'error'; + error?: string; + }; + + 'reload-external-secrets-providers': { + result: 'success' | 'error'; + error?: string; + }; + + // #endregion + + // #region Worker view + + 'get-worker-id': never; + + 'get-worker-status': WorkerStatus; + + // #endregion +}; diff --git a/packages/cli/src/events/queue-metrics-event-map.ts b/packages/cli/src/events/maps/queue-metrics.event-map.ts similarity index 100% rename from packages/cli/src/events/queue-metrics-event-map.ts rename to packages/cli/src/events/maps/queue-metrics.event-map.ts diff --git a/packages/cli/src/events/relay-event-map.ts b/packages/cli/src/events/maps/relay.event-map.ts similarity index 99% rename from packages/cli/src/events/relay-event-map.ts rename to packages/cli/src/events/maps/relay.event-map.ts index a53a36842e..a495820283 100644 --- a/packages/cli/src/events/relay-event-map.ts +++ b/packages/cli/src/events/maps/relay.event-map.ts @@ -11,7 +11,7 @@ import type { ProjectRole } from '@/databases/entities/project-relation'; import type { GlobalRole } from '@/databases/entities/user'; import type { IWorkflowDb } from '@/interfaces'; -import type { AiEventMap } from './ai-event-map'; +import type { AiEventMap } from './ai.event-map'; export type UserLike = { id: string; diff --git a/packages/cli/src/events/event-relay.ts b/packages/cli/src/events/relays/event-relay.ts similarity index 81% rename from packages/cli/src/events/event-relay.ts rename to packages/cli/src/events/relays/event-relay.ts index 3202b69c15..13e7dc01be 100644 --- a/packages/cli/src/events/event-relay.ts +++ b/packages/cli/src/events/relays/event-relay.ts @@ -1,8 +1,7 @@ import { Service } from 'typedi'; -import type { RelayEventMap } from '@/events/relay-event-map'; - -import { EventService } from './event.service'; +import { EventService } from '@/events/event.service'; +import type { RelayEventMap } from '@/events/maps/relay.event-map'; @Service() export class EventRelay { diff --git a/packages/cli/src/events/log-streaming-event-relay.ts b/packages/cli/src/events/relays/log-streaming.event-relay.ts similarity index 98% rename from packages/cli/src/events/log-streaming-event-relay.ts rename to packages/cli/src/events/relays/log-streaming.event-relay.ts index 788e5e50c4..c65af2874c 100644 --- a/packages/cli/src/events/log-streaming-event-relay.ts +++ b/packages/cli/src/events/relays/log-streaming.event-relay.ts @@ -3,10 +3,9 @@ import { Service } from 'typedi'; import { Redactable } from '@/decorators/redactable'; import { MessageEventBus } from '@/eventbus/message-event-bus/message-event-bus'; -import { EventRelay } from '@/events/event-relay'; -import type { RelayEventMap } from '@/events/relay-event-map'; - -import { EventService } from './event.service'; +import { EventService } from '@/events/event.service'; +import type { RelayEventMap } from '@/events/maps/relay.event-map'; +import { EventRelay } from '@/events/relays/event-relay'; @Service() export class LogStreamingEventRelay extends EventRelay { diff --git a/packages/cli/src/events/telemetry-event-relay.ts b/packages/cli/src/events/relays/telemetry.event-relay.ts similarity index 99% rename from packages/cli/src/events/telemetry-event-relay.ts rename to packages/cli/src/events/relays/telemetry.event-relay.ts index 82beb17198..c813926bf1 100644 --- a/packages/cli/src/events/telemetry-event-relay.ts +++ b/packages/cli/src/events/relays/telemetry.event-relay.ts @@ -12,14 +12,14 @@ import { ProjectRelationRepository } from '@/databases/repositories/project-rela import { SharedWorkflowRepository } from '@/databases/repositories/shared-workflow.repository'; import { WorkflowRepository } from '@/databases/repositories/workflow.repository'; import { EventService } from '@/events/event.service'; -import type { RelayEventMap } from '@/events/relay-event-map'; +import type { RelayEventMap } from '@/events/maps/relay.event-map'; import { determineFinalExecutionStatus } from '@/execution-lifecycle-hooks/shared/shared-hook-functions'; import type { IExecutionTrackProperties } from '@/interfaces'; import { License } from '@/license'; import { NodeTypes } from '@/node-types'; import { EventRelay } from './event-relay'; -import { Telemetry } from '../telemetry'; +import { Telemetry } from '../../telemetry'; @Service() export class TelemetryEventRelay extends EventRelay { diff --git a/packages/cli/src/scaling/pubsub/pubsub.types.ts b/packages/cli/src/scaling/pubsub/pubsub.types.ts index 13643440fb..ac83659212 100644 --- a/packages/cli/src/scaling/pubsub/pubsub.types.ts +++ b/packages/cli/src/scaling/pubsub/pubsub.types.ts @@ -1,6 +1,4 @@ -import type { PushType, WorkerStatus } from '@n8n/api-types'; - -import type { IWorkflowDb } from '@/interfaces'; +import type { PubSubCommandMap, PubSubWorkerResponseMap } from '@/events/maps/pub-sub.event-map'; import type { Resolve } from '@/utlity.types'; import type { COMMAND_PUBSUB_CHANNEL, WORKER_RESPONSE_PUBSUB_CHANNEL } from '../constants'; @@ -20,92 +18,17 @@ export namespace PubSub { // commands // ---------------------------------- - export type CommandMap = { - // #region Lifecycle - - 'reload-license': never; - - 'restart-event-bus': never; - - 'reload-external-secrets-providers': never; - - // #endregion - - // #region Community packages - - 'community-package-install': { - packageName: string; - packageVersion: string; - }; - - 'community-package-update': { - packageName: string; - packageVersion: string; - }; - - 'community-package-uninstall': { - packageName: string; - }; - - // #endregion - - // #region Worker view - - 'get-worker-id': never; - - 'get-worker-status': never; - - // #endregion - - // #region Multi-main setup - - 'add-webhooks-triggers-and-pollers': { - workflowId: string; - }; - - 'remove-triggers-and-pollers': { - workflowId: string; - }; - - 'display-workflow-activation': { - workflowId: string; - }; - - 'display-workflow-deactivation': { - workflowId: string; - }; - - 'display-workflow-activation-error': { - workflowId: string; - errorMessage: string; - }; - - 'relay-execution-lifecycle-event': { - type: PushType; - args: Record; - pushRef: string; - }; - - 'clear-test-webhooks': { - webhookKey: string; - workflowEntity: IWorkflowDb; - pushRef: string; - }; - - // #endregion - }; - - type _ToCommand = { + type _ToCommand = { senderId: string; targets?: string[]; command: CommandKey; - } & (CommandMap[CommandKey] extends never + } & (PubSubCommandMap[CommandKey] extends never ? { payload?: never } // some commands carry no payload - : { payload: CommandMap[CommandKey] }); + : { payload: PubSubCommandMap[CommandKey] }); - type ToCommand = Resolve<_ToCommand>; + type ToCommand = Resolve<_ToCommand>; - namespace Command { + namespace Commands { export type ReloadLicense = ToCommand<'reload-license'>; export type RestartEventBus = ToCommand<'restart-event-bus'>; export type ReloadExternalSecretsProviders = ToCommand<'reload-external-secrets-providers'>; @@ -125,63 +48,39 @@ export namespace PubSub { /** Command sent via the `n8n.commands` pubsub channel. */ export type Command = - | Command.ReloadLicense - | Command.RestartEventBus - | Command.ReloadExternalSecretsProviders - | Command.CommunityPackageInstall - | Command.CommunityPackageUpdate - | Command.CommunityPackageUninstall - | Command.GetWorkerId - | Command.GetWorkerStatus - | Command.AddWebhooksTriggersAndPollers - | Command.RemoveTriggersAndPollers - | Command.DisplayWorkflowActivation - | Command.DisplayWorkflowDeactivation - | Command.DisplayWorkflowActivationError - | Command.RelayExecutionLifecycleEvent - | Command.ClearTestWebhooks; + | Commands.ReloadLicense + | Commands.RestartEventBus + | Commands.ReloadExternalSecretsProviders + | Commands.CommunityPackageInstall + | Commands.CommunityPackageUpdate + | Commands.CommunityPackageUninstall + | Commands.GetWorkerId + | Commands.GetWorkerStatus + | Commands.AddWebhooksTriggersAndPollers + | Commands.RemoveTriggersAndPollers + | Commands.DisplayWorkflowActivation + | Commands.DisplayWorkflowDeactivation + | Commands.DisplayWorkflowActivationError + | Commands.RelayExecutionLifecycleEvent + | Commands.ClearTestWebhooks; // ---------------------------------- // worker responses // ---------------------------------- - export type WorkerResponseMap = { - // #region Lifecycle - - 'restart-event-bus': { - result: 'success' | 'error'; - error?: string; - }; - - 'reload-external-secrets-providers': { - result: 'success' | 'error'; - error?: string; - }; - - // #endregion - - // #region Worker view - - 'get-worker-id': never; - - 'get-worker-status': WorkerStatus; - - // #endregion - }; - - type _ToWorkerResponse = { + type _ToWorkerResponse = { workerId: string; targets?: string[]; command: WorkerResponseKey; - } & (WorkerResponseMap[WorkerResponseKey] extends never + } & (PubSubWorkerResponseMap[WorkerResponseKey] extends never ? { payload?: never } // some responses carry no payload - : { payload: WorkerResponseMap[WorkerResponseKey] }); + : { payload: PubSubWorkerResponseMap[WorkerResponseKey] }); - type ToWorkerResponse = Resolve< + type ToWorkerResponse = Resolve< _ToWorkerResponse >; - namespace WorkerResponse { + namespace WorkerResponses { export type RestartEventBus = ToWorkerResponse<'restart-event-bus'>; export type ReloadExternalSecretsProviders = ToWorkerResponse<'reload-external-secrets-providers'>; @@ -191,8 +90,8 @@ export namespace PubSub { /** Response sent via the `n8n.worker-response` pubsub channel. */ export type WorkerResponse = - | WorkerResponse.RestartEventBus - | WorkerResponse.ReloadExternalSecretsProviders - | WorkerResponse.GetWorkerId - | WorkerResponse.GetWorkerStatus; + | WorkerResponses.RestartEventBus + | WorkerResponses.ReloadExternalSecretsProviders + | WorkerResponses.GetWorkerId + | WorkerResponses.GetWorkerStatus; } diff --git a/packages/cli/src/server.ts b/packages/cli/src/server.ts index 0840714b6a..eeb6cdae46 100644 --- a/packages/cli/src/server.ts +++ b/packages/cli/src/server.ts @@ -21,7 +21,7 @@ import { CredentialsOverwrites } from '@/credentials-overwrites'; import { ControllerRegistry } from '@/decorators'; import { MessageEventBus } from '@/eventbus/message-event-bus/message-event-bus'; import { EventService } from '@/events/event.service'; -import { LogStreamingEventRelay } from '@/events/log-streaming-event-relay'; +import { LogStreamingEventRelay } from '@/events/relays/log-streaming.event-relay'; import type { ICredentialsOverwrite } from '@/interfaces'; import { isLdapEnabled } from '@/ldap/helpers.ee'; import { LoadNodesAndCredentials } from '@/load-nodes-and-credentials'; diff --git a/packages/cli/src/services/orchestration.service.ts b/packages/cli/src/services/orchestration.service.ts index 1ee7c26876..bc42d871c3 100644 --- a/packages/cli/src/services/orchestration.service.ts +++ b/packages/cli/src/services/orchestration.service.ts @@ -3,9 +3,9 @@ import type { WorkflowActivateMode } from 'n8n-workflow'; import Container, { Service } from 'typedi'; import config from '@/config'; +import type { PubSubCommandMap } from '@/events/maps/pub-sub.event-map'; import { Logger } from '@/logger'; import type { Publisher } from '@/scaling/pubsub/publisher.service'; -import type { PubSub } from '@/scaling/pubsub/pubsub.types'; import type { Subscriber } from '@/scaling/pubsub/subscriber.service'; import { MultiMainSetup } from './orchestration/main/multi-main-setup.ee'; @@ -97,9 +97,9 @@ export class OrchestrationService { // pubsub // ---------------------------------- - async publish( + async publish( commandKey: CommandKey, - payload?: PubSub.CommandMap[CommandKey], + payload?: PubSubCommandMap[CommandKey], ) { if (!this.sanityCheck()) return; diff --git a/packages/cli/src/workflow-execute-additional-data.ts b/packages/cli/src/workflow-execute-additional-data.ts index bc39620c20..96ac4e1b1d 100644 --- a/packages/cli/src/workflow-execute-additional-data.ts +++ b/packages/cli/src/workflow-execute-additional-data.ts @@ -40,6 +40,7 @@ import { ActiveExecutions } from '@/active-executions'; import config from '@/config'; import { CredentialsHelper } from '@/credentials-helper'; import { ExecutionRepository } from '@/databases/repositories/execution.repository'; +import type { AiEventMap, AiEventPayload } from '@/events/maps/ai.event-map'; import { ExternalHooks } from '@/external-hooks'; import type { IWorkflowExecuteProcess, @@ -53,7 +54,6 @@ import { findSubworkflowStart, isWorkflowIdValid } from '@/utils'; import * as WorkflowHelpers from '@/workflow-helpers'; import { WorkflowRepository } from './databases/repositories/workflow.repository'; -import type { AiEventMap, AiEventPayload } from './events/ai-event-map'; import { EventService } from './events/event.service'; import { restoreBinaryDataId } from './execution-lifecycle-hooks/restore-binary-data-id'; import { saveExecutionProgress } from './execution-lifecycle-hooks/save-execution-progress'; diff --git a/packages/cli/test/integration/commands/worker.cmd.test.ts b/packages/cli/test/integration/commands/worker.cmd.test.ts index 726c78537e..67d3e7bab1 100644 --- a/packages/cli/test/integration/commands/worker.cmd.test.ts +++ b/packages/cli/test/integration/commands/worker.cmd.test.ts @@ -5,7 +5,7 @@ import { BinaryDataService } from 'n8n-core'; import { Worker } from '@/commands/worker'; import config from '@/config'; import { MessageEventBus } from '@/eventbus/message-event-bus/message-event-bus'; -import { LogStreamingEventRelay } from '@/events/log-streaming-event-relay'; +import { LogStreamingEventRelay } from '@/events/relays/log-streaming.event-relay'; import { ExternalHooks } from '@/external-hooks'; import { ExternalSecretsManager } from '@/external-secrets/external-secrets-manager.ee'; import { License } from '@/license'; diff --git a/packages/cli/test/integration/shared/utils/test-command.ts b/packages/cli/test/integration/shared/utils/test-command.ts index 82effd1818..d0737ddcc1 100644 --- a/packages/cli/test/integration/shared/utils/test-command.ts +++ b/packages/cli/test/integration/shared/utils/test-command.ts @@ -4,7 +4,7 @@ import type { Class } from 'n8n-core'; import type { BaseCommand } from '@/commands/base-command'; import { MessageEventBus } from '@/eventbus/message-event-bus/message-event-bus'; -import { TelemetryEventRelay } from '@/events/telemetry-event-relay'; +import { TelemetryEventRelay } from '@/events/relays/telemetry.event-relay'; import { mockInstance } from '@test/mocking'; import * as testDb from '../test-db'; From bb2895689fb006897bc244271aca6f0bfa1839b9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E0=A4=95=E0=A4=BE=E0=A4=B0=E0=A4=A4=E0=A5=8B=E0=A4=AB?= =?UTF-8?q?=E0=A5=8D=E0=A4=AB=E0=A5=87=E0=A4=B2=E0=A4=B8=E0=A5=8D=E0=A4=95?= =?UTF-8?q?=E0=A5=8D=E0=A4=B0=E0=A4=BF=E0=A4=AA=E0=A5=8D=E0=A4=9F=E2=84=A2?= Date: Mon, 30 Sep 2024 10:57:25 +0200 Subject: [PATCH 003/304] feat(editor): Overhaul document title management (#10999) --- .../components/MainHeader/WorkflowDetails.vue | 6 ++-- .../src/components/WorkerList.ee.vue | 5 +-- .../__tests__/useDocumentTitle.test.ts | 31 +++++++++++++++++++ .../__tests__/useRunWorkflow.spec.ts | 5 +-- .../src/composables/useDocumentTitle.ts | 21 +++++++++++++ .../src/composables/usePushConnection.ts | 8 ++--- .../src/composables/useRunWorkflow.ts | 10 +++--- .../src/composables/useTitleChange.ts | 30 ------------------ .../src/composables/useWorkflowHelpers.ts | 14 +++++++++ .../editor-ui/src/stores/settings.store.ts | 4 --- packages/editor-ui/src/utils/htmlUtils.ts | 4 --- .../editor-ui/src/views/CredentialsView.vue | 3 ++ .../editor-ui/src/views/ExecutionsView.vue | 5 +-- packages/editor-ui/src/views/NodeView.v2.vue | 12 +++---- packages/editor-ui/src/views/NodeView.vue | 16 +++++----- .../editor-ui/src/views/ProjectSettings.vue | 3 ++ .../editor-ui/src/views/SettingsApiView.vue | 3 ++ .../src/views/SettingsCommunityNodesView.vue | 3 ++ .../src/views/SettingsExternalSecrets.vue | 3 ++ .../editor-ui/src/views/SettingsLdapView.vue | 3 ++ .../src/views/SettingsLogStreamingView.vue | 3 ++ .../src/views/SettingsPersonalView.vue | 3 ++ .../src/views/SettingsSourceControl.vue | 3 ++ packages/editor-ui/src/views/SettingsSso.vue | 3 ++ .../src/views/SettingsUsageAndPlan.vue | 3 ++ .../editor-ui/src/views/SettingsUsersView.vue | 4 +++ .../src/views/TemplatesCollectionView.vue | 7 +++-- .../src/views/TemplatesSearchView.vue | 5 +-- .../src/views/TemplatesWorkflowView.vue | 7 +++-- .../editor-ui/src/views/VariablesView.vue | 8 ++++- .../editor-ui/src/views/WorkflowsView.vue | 3 ++ 31 files changed, 155 insertions(+), 83 deletions(-) create mode 100644 packages/editor-ui/src/composables/__tests__/useDocumentTitle.test.ts create mode 100644 packages/editor-ui/src/composables/useDocumentTitle.ts delete mode 100644 packages/editor-ui/src/composables/useTitleChange.ts diff --git a/packages/editor-ui/src/components/MainHeader/WorkflowDetails.vue b/packages/editor-ui/src/components/MainHeader/WorkflowDetails.vue index 0cc6dfe58e..4a04c141d2 100644 --- a/packages/editor-ui/src/components/MainHeader/WorkflowDetails.vue +++ b/packages/editor-ui/src/components/MainHeader/WorkflowDetails.vue @@ -34,7 +34,7 @@ import { useWorkflowsStore } from '@/stores/workflows.store'; import { useProjectsStore } from '@/stores/projects.store'; import { saveAs } from 'file-saver'; -import { useTitleChange } from '@/composables/useTitleChange'; +import { useDocumentTitle } from '@/composables/useDocumentTitle'; import { useMessage } from '@/composables/useMessage'; import { useToast } from '@/composables/useToast'; import { getResourcePermissions } from '@/permissions'; @@ -87,7 +87,7 @@ const locale = useI18n(); const telemetry = useTelemetry(); const message = useMessage(); const toast = useToast(); -const titleChange = useTitleChange(); +const documentTitle = useDocumentTitle(); const workflowHelpers = useWorkflowHelpers({ router }); const isTagsEditEnabled = ref(false); @@ -558,7 +558,7 @@ async function onWorkflowMenuSelect(action: WORKFLOW_MENU_ACTIONS): Promise({ releaseChannel: 'stable' }); +vi.mock('@/stores/settings.store', () => ({ + useSettingsStore: vi.fn(() => ({ settings })), +})); + +describe('useDocumentTitle', () => { + it('should set the document title', () => { + const { set } = useDocumentTitle(); + set('Test Title'); + expect(document.title).toBe('Test Title - n8n'); + }); + + it('should reset the document title', () => { + const { set, reset } = useDocumentTitle(); + set('Test Title'); + reset(); + expect(document.title).toBe('Workflow Automation - n8n'); + }); + + it('should use the correct prefix for the release channel', () => { + settings.releaseChannel = 'beta'; + const { set } = useDocumentTitle(); + set('Test Title'); + expect(document.title).toBe('Test Title - n8n[BETA]'); + }); +}); diff --git a/packages/editor-ui/src/composables/__tests__/useRunWorkflow.spec.ts b/packages/editor-ui/src/composables/__tests__/useRunWorkflow.spec.ts index a1583280a9..4b6fde3991 100644 --- a/packages/editor-ui/src/composables/__tests__/useRunWorkflow.spec.ts +++ b/packages/editor-ui/src/composables/__tests__/useRunWorkflow.spec.ts @@ -52,6 +52,7 @@ vi.mock('@/composables/useWorkflowHelpers', () => ({ getCurrentWorkflow: vi.fn(), saveCurrentWorkflow: vi.fn(), getWorkflowDataToSave: vi.fn(), + setDocumentTitle: vi.fn(), }), })); @@ -61,10 +62,6 @@ vi.mock('@/composables/useNodeHelpers', () => ({ }), })); -vi.mock('@/composables/useTitleChange', () => ({ - useTitleChange: vi.fn().mockReturnValue({ titleSet: vi.fn() }), -})); - vi.mock('vue-router', async (importOriginal) => { const { RouterLink } = await importOriginal(); return { diff --git a/packages/editor-ui/src/composables/useDocumentTitle.ts b/packages/editor-ui/src/composables/useDocumentTitle.ts new file mode 100644 index 0000000000..9b0ff97381 --- /dev/null +++ b/packages/editor-ui/src/composables/useDocumentTitle.ts @@ -0,0 +1,21 @@ +import { useSettingsStore } from '@/stores/settings.store'; + +const DEFAULT_TITLE = 'Workflow Automation'; + +export function useDocumentTitle() { + const settingsStore = useSettingsStore(); + const { releaseChannel } = settingsStore.settings; + const suffix = + !releaseChannel || releaseChannel === 'stable' ? 'n8n' : `n8n[${releaseChannel.toUpperCase()}]`; + + const set = (title: string) => { + const sections = [title || DEFAULT_TITLE, suffix]; + document.title = sections.join(' - '); + }; + + const reset = () => { + set(''); + }; + + return { set, reset }; +} diff --git a/packages/editor-ui/src/composables/usePushConnection.ts b/packages/editor-ui/src/composables/usePushConnection.ts index 347fc6d0bb..4ced982610 100644 --- a/packages/editor-ui/src/composables/usePushConnection.ts +++ b/packages/editor-ui/src/composables/usePushConnection.ts @@ -18,7 +18,6 @@ import type { PushMessage, PushPayload } from '@n8n/api-types'; import type { IExecutionResponse, IExecutionsCurrentSummaryExtended } from '@/Interface'; import { useNodeHelpers } from '@/composables/useNodeHelpers'; -import { useTitleChange } from '@/composables/useTitleChange'; import { useToast } from '@/composables/useToast'; import { WORKFLOW_SETTINGS_MODAL_KEY } from '@/constants'; import { getTriggerNodeServiceName } from '@/utils/nodeTypesUtils'; @@ -43,7 +42,6 @@ type IPushDataExecutionFinishedPayload = PushPayload<'executionFinished'>; export function usePushConnection({ router }: { router: ReturnType }) { const workflowHelpers = useWorkflowHelpers({ router }); const nodeHelpers = useNodeHelpers(); - const titleChange = useTitleChange(); const toast = useToast(); const i18n = useI18n(); const telemetry = useTelemetry(); @@ -324,7 +322,7 @@ export function usePushConnection({ router }: { router: ReturnTypeMore info`, @@ -333,7 +331,7 @@ export function usePushConnection({ router }: { router: ReturnType { - const settingsStore = useSettingsStore(); - const { releaseChannel } = settingsStore.settings; - return releaseChannel === 'stable' ? title : `[${releaseChannel.toUpperCase()}] ${title}`; - }; - - const titleSet = (workflow: string, status: WorkflowTitleStatus) => { - let icon = '⚠️'; - if (status === 'EXECUTING') { - icon = '🔄'; - } else if (status === 'IDLE') { - icon = '▶️'; - } - - window.document.title = prependBeta(`n8n - ${icon} ${workflow}`); - }; - - const titleReset = () => { - window.document.title = prependBeta('n8n - Workflow Automation'); - }; - - return { - titleSet, - titleReset, - }; -} diff --git a/packages/editor-ui/src/composables/useWorkflowHelpers.ts b/packages/editor-ui/src/composables/useWorkflowHelpers.ts index 77d8abeb14..6848ee7ff4 100644 --- a/packages/editor-ui/src/composables/useWorkflowHelpers.ts +++ b/packages/editor-ui/src/composables/useWorkflowHelpers.ts @@ -37,6 +37,7 @@ import type { IWorkflowDataUpdate, IWorkflowDb, TargetItem, + WorkflowTitleStatus, XYPosition, } from '@/Interface'; @@ -57,6 +58,7 @@ import { getSourceItems } from '@/utils/pairedItemUtils'; import { v4 as uuid } from 'uuid'; import { useSettingsStore } from '@/stores/settings.store'; import { getCredentialTypeName, isCredentialOnlyNodeType } from '@/utils/credentialOnlyNodes'; +import { useDocumentTitle } from '@/composables/useDocumentTitle'; import { useExternalHooks } from '@/composables/useExternalHooks'; import { useCanvasStore } from '@/stores/canvas.store'; import { useSourceControlStore } from '@/stores/sourceControl.store'; @@ -458,6 +460,17 @@ export function useWorkflowHelpers(options: { router: ReturnType { + let icon = '⚠️'; + if (status === 'EXECUTING') { + icon = '🔄'; + } else if (status === 'IDLE') { + icon = '▶️'; + } + documentTitle.set(`${icon} ${workflowName}`); + }; function getNodeTypesMaxCount() { const nodes = workflowsStore.allNodes; @@ -1172,6 +1185,7 @@ export function useWorkflowHelpers(options: { router: ReturnType { ExpressionEvaluatorProxy.setEvaluator(settings.value.expressions.evaluator); - // Re-compute title since settings are now available - useTitleChange().titleReset(); - initialized.value = true; } catch (e) { showToast({ diff --git a/packages/editor-ui/src/utils/htmlUtils.ts b/packages/editor-ui/src/utils/htmlUtils.ts index 1fea9b7833..cf4bb85c6c 100644 --- a/packages/editor-ui/src/utils/htmlUtils.ts +++ b/packages/editor-ui/src/utils/htmlUtils.ts @@ -48,10 +48,6 @@ export const sanitizeIfString = (message: T): string | T => { return message; }; -export function setPageTitle(title: string) { - window.document.title = title; -} - export function convertRemToPixels(rem: string) { return parseInt(rem, 10) * parseFloat(getComputedStyle(document.documentElement).fontSize); } diff --git a/packages/editor-ui/src/views/CredentialsView.vue b/packages/editor-ui/src/views/CredentialsView.vue index 31040e2b39..c544f998aa 100644 --- a/packages/editor-ui/src/views/CredentialsView.vue +++ b/packages/editor-ui/src/views/CredentialsView.vue @@ -18,6 +18,7 @@ import ProjectTabs from '@/components/Projects/ProjectTabs.vue'; import useEnvironmentsStore from '@/stores/environments.ee.store'; import { useSettingsStore } from '@/stores/settings.store'; import { getResourcePermissions } from '@/permissions'; +import { useDocumentTitle } from '@/composables/useDocumentTitle'; export default defineComponent({ name: 'CredentialsView', @@ -35,6 +36,7 @@ export default defineComponent({ }, sourceControlStoreUnsubscribe: () => {}, loading: false, + documentTitle: useDocumentTitle(), }; }, computed: { @@ -86,6 +88,7 @@ export default defineComponent({ }, }, mounted() { + this.documentTitle.set(this.$locale.baseText('credentials.heading')); this.sourceControlStoreUnsubscribe = this.sourceControlStore.$onAction(({ name, after }) => { if (name === 'pullWorkfolder' && after) { after(() => { diff --git a/packages/editor-ui/src/views/ExecutionsView.vue b/packages/editor-ui/src/views/ExecutionsView.vue index bdb37c46ac..7ac9205ce7 100644 --- a/packages/editor-ui/src/views/ExecutionsView.vue +++ b/packages/editor-ui/src/views/ExecutionsView.vue @@ -1,13 +1,13 @@ diff --git a/packages/editor-ui/src/views/SettingsApiView.vue b/packages/editor-ui/src/views/SettingsApiView.vue index 0278063718..2bb67d0440 100644 --- a/packages/editor-ui/src/views/SettingsApiView.vue +++ b/packages/editor-ui/src/views/SettingsApiView.vue @@ -3,6 +3,7 @@ import { defineComponent } from 'vue'; import type { ApiKey, IUser } from '@/Interface'; import { useToast } from '@/composables/useToast'; import { useMessage } from '@/composables/useMessage'; +import { useDocumentTitle } from '@/composables/useDocumentTitle'; import CopyInput from '@/components/CopyInput.vue'; import { mapStores } from 'pinia'; @@ -23,6 +24,7 @@ export default defineComponent({ ...useToast(), ...useMessage(), ...useUIStore(), + documentTitle: useDocumentTitle(), }; }, data() { @@ -35,6 +37,7 @@ export default defineComponent({ }; }, mounted() { + this.documentTitle.set(this.$locale.baseText('settings.api')); if (!this.isPublicApiEnabled) return; void this.getApiKeys(); diff --git a/packages/editor-ui/src/views/SettingsCommunityNodesView.vue b/packages/editor-ui/src/views/SettingsCommunityNodesView.vue index 0c0ceafbb1..79ce24a468 100644 --- a/packages/editor-ui/src/views/SettingsCommunityNodesView.vue +++ b/packages/editor-ui/src/views/SettingsCommunityNodesView.vue @@ -5,6 +5,7 @@ import { } from '@/constants'; import CommunityPackageCard from '@/components/CommunityPackageCard.vue'; import { useToast } from '@/composables/useToast'; +import { useDocumentTitle } from '@/composables/useDocumentTitle'; import type { PublicInstalledPackage } from 'n8n-workflow'; import { useCommunityNodesStore } from '@/stores/communityNodes.store'; @@ -31,6 +32,7 @@ const externalHooks = useExternalHooks(); const i18n = useI18n(); const telemetry = useTelemetry(); const toast = useToast(); +const documentTitle = useDocumentTitle(); const communityNodesStore = useCommunityNodesStore(); const uiStore = useUIStore(); @@ -85,6 +87,7 @@ onBeforeMount(() => { }); onMounted(async () => { + documentTitle.set(i18n.baseText('settings.communityNodes')); try { loading.value = true; await communityNodesStore.fetchInstalledPackages(); diff --git a/packages/editor-ui/src/views/SettingsExternalSecrets.vue b/packages/editor-ui/src/views/SettingsExternalSecrets.vue index a8fe89fb6c..30aaebeae1 100644 --- a/packages/editor-ui/src/views/SettingsExternalSecrets.vue +++ b/packages/editor-ui/src/views/SettingsExternalSecrets.vue @@ -2,6 +2,7 @@ import { useUIStore } from '@/stores/ui.store'; import { useI18n } from '@/composables/useI18n'; import { useToast } from '@/composables/useToast'; +import { useDocumentTitle } from '@/composables/useDocumentTitle'; import { useExternalSecretsStore } from '@/stores/externalSecrets.ee.store'; import { computed, onMounted } from 'vue'; import ExternalSecretsProviderCard from '@/components/ExternalSecretsProviderCard.ee.vue'; @@ -11,6 +12,7 @@ const i18n = useI18n(); const uiStore = useUIStore(); const externalSecretsStore = useExternalSecretsStore(); const toast = useToast(); +const documentTitle = useDocumentTitle(); const sortedProviders = computed(() => { return ([...externalSecretsStore.providers] as ExternalSecretsProvider[]).sort((a, b) => { @@ -19,6 +21,7 @@ const sortedProviders = computed(() => { }); onMounted(() => { + documentTitle.set(i18n.baseText('settings.externalSecrets.title')); if (!externalSecretsStore.isEnterpriseExternalSecretsEnabled) return; try { void externalSecretsStore.fetchAllSecrets(); diff --git a/packages/editor-ui/src/views/SettingsLdapView.vue b/packages/editor-ui/src/views/SettingsLdapView.vue index b0c497e817..41227c854e 100644 --- a/packages/editor-ui/src/views/SettingsLdapView.vue +++ b/packages/editor-ui/src/views/SettingsLdapView.vue @@ -5,6 +5,7 @@ import { capitalizeFirstLetter } from '@/utils/htmlUtils'; import { convertToDisplayDate } from '@/utils/typesUtils'; import { useToast } from '@/composables/useToast'; import { useMessage } from '@/composables/useMessage'; +import { useDocumentTitle } from '@/composables/useDocumentTitle'; import type { ILdapConfig, ILdapSyncData, @@ -65,6 +66,7 @@ type CellClassStyleMethodParams = { const toast = useToast(); const i18n = useI18n(); const message = useMessage(); +const documentTitle = useDocumentTitle(); const settingsStore = useSettingsStore(); const uiStore = useUIStore(); @@ -585,6 +587,7 @@ const reloadLdapSynchronizations = async () => { }; onMounted(async () => { + documentTitle.set(i18n.baseText('settings.ldap')); if (!isLDAPFeatureEnabled.value) return; await getLdapConfig(); }); diff --git a/packages/editor-ui/src/views/SettingsLogStreamingView.vue b/packages/editor-ui/src/views/SettingsLogStreamingView.vue index b39f5fd642..fae75b2604 100644 --- a/packages/editor-ui/src/views/SettingsLogStreamingView.vue +++ b/packages/editor-ui/src/views/SettingsLogStreamingView.vue @@ -13,6 +13,7 @@ import type { MessageEventBusDestinationOptions } from 'n8n-workflow'; import { deepCopy, defaultMessageEventBusDestinationOptions } from 'n8n-workflow'; import EventDestinationCard from '@/components/SettingsLogStreaming/EventDestinationCard.ee.vue'; import { createEventBus } from 'n8n-design-system/utils'; +import { useDocumentTitle } from '@/composables/useDocumentTitle'; export default defineComponent({ name: 'SettingsLogStreamingView', @@ -26,9 +27,11 @@ export default defineComponent({ destinations: Array, disableLicense: false, allDestinations: [] as MessageEventBusDestinationOptions[], + documentTitle: useDocumentTitle(), }; }, async mounted() { + this.documentTitle.set(this.$locale.baseText('settings.log-streaming.heading')); if (!this.isLicensed) return; // Prepare credentialsStore so modals can pick up credentials diff --git a/packages/editor-ui/src/views/SettingsPersonalView.vue b/packages/editor-ui/src/views/SettingsPersonalView.vue index 0ddbe6e5a5..af1c92519d 100644 --- a/packages/editor-ui/src/views/SettingsPersonalView.vue +++ b/packages/editor-ui/src/views/SettingsPersonalView.vue @@ -2,6 +2,7 @@ import { ref, computed, onMounted, onBeforeUnmount } from 'vue'; import { useI18n } from '@/composables/useI18n'; import { useToast } from '@/composables/useToast'; +import { useDocumentTitle } from '@/composables/useDocumentTitle'; import type { IFormInputs, IUser, ThemeOption } from '@/Interface'; import { CHANGE_PASSWORD_MODAL_KEY, @@ -28,6 +29,7 @@ type UserBasicDetailsWithMfa = UserBasicDetailsForm & { const i18n = useI18n(); const { showToast, showError } = useToast(); +const documentTitle = useDocumentTitle(); const hasAnyBasicInfoChanges = ref(false); const formInputs = ref(null); @@ -80,6 +82,7 @@ const hasAnyChanges = computed(() => { }); onMounted(() => { + documentTitle.set(i18n.baseText('settings.personal.personalSettings')); formInputs.value = [ { name: 'firstName', diff --git a/packages/editor-ui/src/views/SettingsSourceControl.vue b/packages/editor-ui/src/views/SettingsSourceControl.vue index 23505092da..3928556104 100644 --- a/packages/editor-ui/src/views/SettingsSourceControl.vue +++ b/packages/editor-ui/src/views/SettingsSourceControl.vue @@ -8,6 +8,7 @@ import { useToast } from '@/composables/useToast'; import { useLoadingService } from '@/composables/useLoadingService'; import { useI18n } from '@/composables/useI18n'; import { useMessage } from '@/composables/useMessage'; +import { useDocumentTitle } from '@/composables/useDocumentTitle'; import CopyInput from '@/components/CopyInput.vue'; import type { TupleToUnion } from '@/utils/typeHelpers'; import type { SshKeyTypes } from '@/Interface'; @@ -17,6 +18,7 @@ const sourceControlStore = useSourceControlStore(); const uiStore = useUIStore(); const toast = useToast(); const message = useMessage(); +const documentTitle = useDocumentTitle(); const loadingService = useLoadingService(); const isConnected = ref(false); @@ -112,6 +114,7 @@ const initialize = async () => { }; onMounted(async () => { + documentTitle.set(locale.baseText('settings.sourceControl.title')); if (!sourceControlStore.isEnterpriseSourceControlEnabled) return; await initialize(); }); diff --git a/packages/editor-ui/src/views/SettingsSso.vue b/packages/editor-ui/src/views/SettingsSso.vue index a73733392b..a9043c41ae 100644 --- a/packages/editor-ui/src/views/SettingsSso.vue +++ b/packages/editor-ui/src/views/SettingsSso.vue @@ -7,6 +7,7 @@ import { useI18n } from '@/composables/useI18n'; import { useMessage } from '@/composables/useMessage'; import { useToast } from '@/composables/useToast'; import { useTelemetry } from '@/composables/useTelemetry'; +import { useDocumentTitle } from '@/composables/useDocumentTitle'; import { useRootStore } from '@/stores/root.store'; const IdentityProviderSettingsType = { @@ -21,6 +22,7 @@ const ssoStore = useSSOStore(); const uiStore = useUIStore(); const message = useMessage(); const toast = useToast(); +const documentTitle = useDocumentTitle(); const ssoActivatedLabel = computed(() => ssoStore.isSamlLoginEnabled @@ -144,6 +146,7 @@ const isToggleSsoDisabled = computed(() => { }); onMounted(async () => { + documentTitle.set(i18n.baseText('settings.sso.title')); if (!ssoStore.isEnterpriseSamlEnabled) { return; } diff --git a/packages/editor-ui/src/views/SettingsUsageAndPlan.vue b/packages/editor-ui/src/views/SettingsUsageAndPlan.vue index 85ac34af9a..f562bf7bb9 100644 --- a/packages/editor-ui/src/views/SettingsUsageAndPlan.vue +++ b/packages/editor-ui/src/views/SettingsUsageAndPlan.vue @@ -7,6 +7,7 @@ import { telemetry } from '@/plugins/telemetry'; import { i18n as locale } from '@/plugins/i18n'; import { useUIStore } from '@/stores/ui.store'; import { useToast } from '@/composables/useToast'; +import { useDocumentTitle } from '@/composables/useDocumentTitle'; import { hasPermission } from '@/utils/rbac/permissions'; const usageStore = useUsageStore(); @@ -14,6 +15,7 @@ const route = useRoute(); const router = useRouter(); const uiStore = useUIStore(); const toast = useToast(); +const documentTitle = useDocumentTitle(); const queryParamCallback = ref( `callback=${encodeURIComponent(`${window.location.origin}${window.location.pathname}`)}`, @@ -64,6 +66,7 @@ const onLicenseActivation = async () => { }; onMounted(async () => { + documentTitle.set(locale.baseText('settings.usageAndPlan.title')); usageStore.setLoading(true); if (route.query.key) { try { diff --git a/packages/editor-ui/src/views/SettingsUsersView.vue b/packages/editor-ui/src/views/SettingsUsersView.vue index a0fea3b4b8..e29bb3e755 100644 --- a/packages/editor-ui/src/views/SettingsUsersView.vue +++ b/packages/editor-ui/src/views/SettingsUsersView.vue @@ -12,6 +12,7 @@ import { useClipboard } from '@/composables/useClipboard'; import type { UpdateGlobalRolePayload } from '@/api/users'; import { computed, onMounted } from 'vue'; import { useI18n } from '@/composables/useI18n'; +import { useDocumentTitle } from '@/composables/useDocumentTitle'; const clipboard = useClipboard(); const { showToast, showError } = useToast(); @@ -20,6 +21,7 @@ const settingsStore = useSettingsStore(); const uiStore = useUIStore(); const usersStore = useUsersStore(); const ssoStore = useSSOStore(); +const documentTitle = useDocumentTitle(); const i18n = useI18n(); @@ -28,6 +30,8 @@ const showUMSetupWarning = computed(() => { }); onMounted(async () => { + documentTitle.set(i18n.baseText('settings.users')); + if (!showUMSetupWarning.value) { await usersStore.fetchUsers(); } diff --git a/packages/editor-ui/src/views/TemplatesCollectionView.vue b/packages/editor-ui/src/views/TemplatesCollectionView.vue index 535f664967..5432a8ed54 100644 --- a/packages/editor-ui/src/views/TemplatesCollectionView.vue +++ b/packages/editor-ui/src/views/TemplatesCollectionView.vue @@ -13,7 +13,7 @@ import { useNodeTypesStore } from '@/stores/nodeTypes.store'; import { isFullTemplatesCollection } from '@/utils/templates/typeGuards'; import { useRoute, useRouter } from 'vue-router'; import { useTelemetry } from '@/composables/useTelemetry'; -import { setPageTitle } from '@/utils/htmlUtils'; +import { useDocumentTitle } from '@/composables/useDocumentTitle'; import { useI18n } from '@/composables/useI18n'; const externalHooks = useExternalHooks(); @@ -25,6 +25,7 @@ const route = useRoute(); const router = useRouter(); const telemetry = useTelemetry(); const i18n = useI18n(); +const documentTitle = useDocumentTitle(); const loading = ref(true); const notFoundError = ref(false); @@ -89,9 +90,9 @@ watch( () => collection.value, () => { if (collection.value && 'full' in collection.value && collection.value.full) { - setPageTitle(`n8n - Template collection: ${collection.value.name}`); + documentTitle.set(`Template collection: ${collection.value.name}`); } else { - setPageTitle('n8n - Templates'); + documentTitle.set('Templates'); } }, ); diff --git a/packages/editor-ui/src/views/TemplatesSearchView.vue b/packages/editor-ui/src/views/TemplatesSearchView.vue index 86c2b789f4..abb30a3586 100644 --- a/packages/editor-ui/src/views/TemplatesSearchView.vue +++ b/packages/editor-ui/src/views/TemplatesSearchView.vue @@ -13,7 +13,6 @@ import type { ITemplatesCategory, } from '@/Interface'; import type { IDataObject } from 'n8n-workflow'; -import { setPageTitle } from '@/utils/htmlUtils'; import { CREATOR_HUB_URL, VIEWS } from '@/constants'; import { useSettingsStore } from '@/stores/settings.store'; import { useUsersStore } from '@/stores/users.store'; @@ -22,6 +21,7 @@ import { useUIStore } from '@/stores/ui.store'; import { useToast } from '@/composables/useToast'; import { usePostHog } from '@/stores/posthog.store'; import { useDebounce } from '@/composables/useDebounce'; +import { useDocumentTitle } from '@/composables/useDocumentTitle'; interface ISearchEvent { search_string: string; @@ -55,6 +55,7 @@ export default defineComponent({ return { callDebounced, ...useToast(), + documentTitle: useDocumentTitle(), }; }, data() { @@ -116,7 +117,7 @@ export default defineComponent({ }, }, async mounted() { - setPageTitle('n8n - Templates'); + this.documentTitle.set('Templates'); await this.loadCategories(); void this.loadWorkflowsAndCollections(true); void this.usersStore.showPersonalizationSurvey(); diff --git a/packages/editor-ui/src/views/TemplatesWorkflowView.vue b/packages/editor-ui/src/views/TemplatesWorkflowView.vue index 1bfb25af3c..8b86da9e6c 100644 --- a/packages/editor-ui/src/views/TemplatesWorkflowView.vue +++ b/packages/editor-ui/src/views/TemplatesWorkflowView.vue @@ -1,6 +1,5 @@